diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..ced3b155b --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,1871 @@ +{ + "files": [ + "website/src/pages/credits.mdx" + ], + "imageSize": 75, + "commit": false, + "badgeTemplate": "Contributors", + "contributors": [ + { + "login": "leaanthony", + "name": "Lea Anthony", + "avatar_url": "https://avatars.githubusercontent.com/u/1943904?v=4", + "profile": "https://github.com/leaanthony", + "contributions": [ + "code", + "ideas", + "design", + "content", + "example", + "mentoring", + "projectManagement", + "tool", + "bug", + "blog", + "maintenance", + "platform", + "review", + "question", + "research", + "test", + "tutorial", + "talk", + "review", + "doc" + ] + }, + { + "login": "stffabi", + "name": "stffabi", + "avatar_url": "https://avatars.githubusercontent.com/u/9464631?v=4", + "profile": "https://github.com/stffabi", + "contributions": [ + "code", + "ideas", + "design", + "bug", + "maintenance", + "platform", + "review", + "question", + "research", + "review", + "doc", + "test" + ] + }, + { + "login": "tmclane", + "name": "Travis McLane", + "avatar_url": "https://avatars.githubusercontent.com/u/511975?v=4", + "profile": "https://github.com/tmclane", + "contributions": [ + "code", + "research", + "platform", + "ideas", + "bug", + "review", + "test", + "question", + "doc" + ] + }, + { + "login": "misitebao", + "name": "Misite Bao", + "avatar_url": "https://avatars.githubusercontent.com/u/28185258?v=4", + "profile": "https://misitebao.com/", + "contributions": [ + "doc", + "translation", + "research", + "maintenance" + ] + }, + { + "login": "bh90210", + "name": "Byron Chris", + "avatar_url": "https://avatars.githubusercontent.com/u/22690219?v=4", + "profile": "https://github.com/bh90210", + "contributions": [ + "code", + "research", + "maintenance", + "bug", + "review", + "test", + "question", + "ideas", + "design", + "platform", + "infra" + ] + }, + { + "login": "konez2k", + "name": "konez2k", + "avatar_url": "https://avatars.githubusercontent.com/u/32417933?v=4", + "profile": "https://github.com/konez2k", + "contributions": [ + "code", + "platform", + "ideas" + ] + }, + { + "login": "dedo1911", + "name": "Dario Emerson", + "avatar_url": "https://avatars.githubusercontent.com/u/1364496?v=4", + "profile": "https://github.com/dedo1911", + "contributions": [ + "code", + "bug", + "ideas", + "test" + ] + }, + { + "login": "ianmjones", + "name": "Ian M. Jones", + "avatar_url": "https://avatars.githubusercontent.com/u/4710?v=4", + "profile": "https://ianmjones.com/", + "contributions": [ + "code", + "bug", + "ideas", + "test", + "review", + "platform" + ] + }, + { + "login": "marktohark", + "name": "marktohark", + "avatar_url": "https://avatars.githubusercontent.com/u/19359934?v=4", + "profile": "https://github.com/marktohark", + "contributions": [ + "code" + ] + }, + { + "login": "rh12503", + "name": "Ryan H", + "avatar_url": "https://avatars.githubusercontent.com/u/48951973?v=4", + "profile": "https://github.com/rh12503", + "contributions": [ + "code" + ] + }, + { + "login": "sircodemane", + "name": "Cody Bentley", + "avatar_url": "https://avatars.githubusercontent.com/u/6968902?v=4", + "profile": "https://codybentley.dev/", + "contributions": [ + "code", + "platform", + "ideas", + "financial" + ] + }, + { + "login": "napalu", + "name": "Florent", + "avatar_url": "https://avatars.githubusercontent.com/u/6690378?v=4", + "profile": "https://github.com/napalu", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "akhudek", + "name": "Alexander Hudek", + "avatar_url": "https://avatars.githubusercontent.com/u/147633?v=4", + "profile": "https://github.com/akhudek", + "contributions": [ + "code", + "financial" + ] + }, + { + "login": "timkippdev", + "name": "Tim Kipp", + "avatar_url": "https://avatars.githubusercontent.com/u/37030721?v=4", + "profile": "https://twitter.com/timkippdev", + "contributions": [ + "code" + ] + }, + { + "login": "gelleson", + "name": "Altynbek Kaliakbarov", + "avatar_url": "https://avatars.githubusercontent.com/u/44272887?v=4", + "profile": "https://github.com/gelleson", + "contributions": [ + "code" + ] + }, + { + "login": "Chronophylos", + "name": "Nikolai Zimmermann", + "avatar_url": "https://avatars.githubusercontent.com/u/14890588?v=4", + "profile": "https://github.com/Chronophylos", + "contributions": [ + "code" + ] + }, + { + "login": "k-muchmore", + "name": "k-muchmore", + "avatar_url": "https://avatars.githubusercontent.com/u/16393095?v=4", + "profile": "https://github.com/k-muchmore", + "contributions": [ + "code" + ] + }, + { + "login": "Snider", + "name": "Snider", + "avatar_url": "https://avatars.githubusercontent.com/u/631881?v=4", + "profile": "https://peakd.com/@snider", + "contributions": [ + "code", + "ideas", + "doc", + "financial" + ] + }, + { + "login": "albert-sun", + "name": "Albert Sun", + "avatar_url": "https://avatars.githubusercontent.com/u/54585592?v=4", + "profile": "https://github.com/albert-sun", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "adalessa", + "name": "Ariel", + "avatar_url": "https://avatars.githubusercontent.com/u/7914601?v=4", + "profile": "https://github.com/adalessa", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "ilgityildirim", + "name": "Ilgıt Yıldırım", + "avatar_url": "https://avatars.githubusercontent.com/u/4365245?v=4", + "profile": "https://triplebits.com/", + "contributions": [ + "code", + "bug", + "financial" + ] + }, + { + "login": "Vaelatern", + "name": "Toyam Cox", + "avatar_url": "https://avatars.githubusercontent.com/u/7906072?v=4", + "profile": "https://github.com/Vaelatern", + "contributions": [ + "code", + "platform", + "bug" + ] + }, + { + "login": "hi019", + "name": "hi019", + "avatar_url": "https://avatars.githubusercontent.com/u/65871571?v=4", + "profile": "https://github.com/hi019", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "artooro", + "name": "Arthur Wiebe", + "avatar_url": "https://avatars.githubusercontent.com/u/393395?v=4", + "profile": "https://artooro.com/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "aayush420", + "name": "Balakrishna Prasad Ganne", + "avatar_url": "https://avatars.githubusercontent.com/u/16898783?v=4", + "profile": "https://sectcs.com/", + "contributions": [ + "code" + ] + }, + { + "login": "BillBuilt", + "name": "BillBuilt", + "avatar_url": "https://avatars.githubusercontent.com/u/28831382?v=4", + "profile": "https://github.com/BillBuilt", + "contributions": [ + "code", + "platform", + "ideas", + "question", + "financial" + ] + }, + { + "login": "Juneezee", + "name": "Eng Zer Jun", + "avatar_url": "https://avatars.githubusercontent.com/u/20135478?v=4", + "profile": "https://github.com/Juneezee", + "contributions": [ + "maintenance", + "code" + ] + }, + { + "login": "LGiki", + "name": "LGiki", + "avatar_url": "https://avatars.githubusercontent.com/u/20807713?v=4", + "profile": "https://lgiki.net/", + "contributions": [ + "doc" + ] + }, + { + "login": "lontten", + "name": "Lontten", + "avatar_url": "https://avatars.githubusercontent.com/u/30745595?v=4", + "profile": "https://github.com/lontten", + "contributions": [ + "doc" + ] + }, + { + "login": "phoenix147", + "name": "Lukas Crepaz", + "avatar_url": "https://avatars.githubusercontent.com/u/809358?v=4", + "profile": "https://github.com/phoenix147", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "marcus-crane", + "name": "Marcus Crane", + "avatar_url": "https://avatars.githubusercontent.com/u/14816406?v=4", + "profile": "https://utf9k.net/", + "contributions": [ + "bug", + "doc", + "financial" + ] + }, + { + "login": "qaisjp", + "name": "Qais Patankar", + "avatar_url": "https://avatars.githubusercontent.com/u/923242?v=4", + "profile": "https://qaisjp.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "Wakeful-Cloud", + "name": "Wakeful-Cloud", + "avatar_url": "https://avatars.githubusercontent.com/u/38930607?v=4", + "profile": "https://wakefulcloud.dev/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "Lyimmi", + "name": "Zámbó, Levente", + "avatar_url": "https://avatars.githubusercontent.com/u/8627125?v=4", + "profile": "https://github.com/Lyimmi", + "contributions": [ + "code", + "platform", + "bug", + "test" + ] + }, + { + "login": "Ironpark", + "name": "Ironpark", + "avatar_url": "https://avatars.githubusercontent.com/u/4973597?v=4", + "profile": "https://github.com/Ironpark", + "contributions": [ + "code", + "ideas" + ] + }, + { + "login": "mondy", + "name": "mondy", + "avatar_url": "https://avatars.githubusercontent.com/u/3961824?v=4", + "profile": "https://github.com/mondy", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "redraskal", + "name": "Benjamin Ryan", + "avatar_url": "https://avatars.githubusercontent.com/u/6241454?v=4", + "profile": "https://ryben.dev/", + "contributions": [ + "bug" + ] + }, + { + "login": "fallendusk", + "name": "fallendusk", + "avatar_url": "https://avatars.githubusercontent.com/u/565631?v=4", + "profile": "https://github.com/fallendusk", + "contributions": [ + "platform", + "code" + ] + }, + { + "login": "matryer", + "name": "Mat Ryer", + "avatar_url": "https://avatars.githubusercontent.com/u/101659?v=4", + "profile": "https://twitter.com/matryer", + "contributions": [ + "code", + "ideas", + "bug" + ] + }, + { + "login": "abtin", + "name": "Abtin", + "avatar_url": "https://avatars.githubusercontent.com/u/441372?v=4", + "profile": "https://github.com/abtin", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "lanzafame", + "name": "Adrian Lanzafame", + "avatar_url": "https://avatars.githubusercontent.com/u/5924712?v=4", + "profile": "https://github.com/lanzafame", + "contributions": [ + "platform", + "code" + ] + }, + { + "login": "polikow", + "name": "Aleksey Polyakov", + "avatar_url": "https://avatars.githubusercontent.com/u/58259700?v=4", + "profile": "https://github.com/polikow", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "alexmat", + "name": "Alexander Matviychuk", + "avatar_url": "https://avatars.githubusercontent.com/u/745421?v=4", + "profile": "https://github.com/alexmat", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "AlienRecall", + "name": "AlienRecall", + "avatar_url": "https://avatars.githubusercontent.com/u/68950287?v=4", + "profile": "https://github.com/AlienRecall", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "achhabra2", + "name": "Aman", + "avatar_url": "https://avatars.githubusercontent.com/u/17457975?v=4", + "profile": "https://blog.checkyo.tech/", + "contributions": [ + "doc" + ] + }, + { + "login": "amaury-tobias", + "name": "Amaury Tobias Quiroz", + "avatar_url": "https://avatars.githubusercontent.com/u/37311888?v=4", + "profile": "https://github.com/amaury-tobias", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "andywenk", + "name": "Andreas Wenk", + "avatar_url": "https://avatars.githubusercontent.com/u/51517?v=4", + "profile": "http://blog.nms.de/", + "contributions": [ + "doc" + ] + }, + { + "login": "stankovic98", + "name": "Antonio Stanković", + "avatar_url": "https://avatars.githubusercontent.com/u/29852655?v=4", + "profile": "https://github.com/stankovic98", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "antimatter96", + "name": "Arpit Jain", + "avatar_url": "https://avatars.githubusercontent.com/u/12068176?v=4", + "profile": "https://github.com/antimatter96", + "contributions": [ + "doc" + ] + }, + { + "login": "aschey", + "name": "Austin Schey", + "avatar_url": "https://avatars.githubusercontent.com/u/5882266?v=4", + "profile": "https://github.com/aschey", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "benjamin-thomas", + "name": "Benjamin Thomas", + "avatar_url": "https://avatars.githubusercontent.com/u/1557738?v=4", + "profile": "https://github.com/benjamin-thomas", + "contributions": [ + "code", + "platform", + "ideas" + ] + }, + { + "login": "bt", + "name": "Bertram Truong", + "avatar_url": "https://avatars.githubusercontent.com/u/1100843?v=4", + "profile": "https://www.bertramtruong.com/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "TechplexEngineer", + "name": "Blake Bourque", + "avatar_url": "https://avatars.githubusercontent.com/u/175873?v=4", + "profile": "http://techwizworld.net/", + "contributions": [ + "doc" + ] + }, + { + "login": "raitonoberu", + "name": "Denis", + "avatar_url": "https://avatars.githubusercontent.com/u/64320078?v=4", + "profile": "http://vk.com/raitonoberu", + "contributions": [ + "doc" + ] + }, + { + "login": "diogox", + "name": "diogox", + "avatar_url": "https://avatars.githubusercontent.com/u/13244408?v=4", + "profile": "https://github.com/diogox", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "kyoto44", + "name": "Dmitry Gomzyakov", + "avatar_url": "https://avatars.githubusercontent.com/u/17720761?v=4", + "profile": "https://github.com/kyoto44", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "edwardbrowncross", + "name": "Edward Browncross", + "avatar_url": "https://avatars.githubusercontent.com/u/35063432?v=4", + "profile": "https://github.com/edwardbrowncross", + "contributions": [ + "code" + ] + }, + { + "login": "elie-g", + "name": "Elie Grenon", + "avatar_url": "https://avatars.githubusercontent.com/u/14944216?v=4", + "profile": "http://pr0gramming.ca/", + "contributions": [ + "code" + ] + }, + { + "login": "fdidron", + "name": "Florian Didron", + "avatar_url": "https://avatars.githubusercontent.com/u/1848786?v=4", + "profile": "https://github.com/fdidron", + "contributions": [ + "code", + "bug", + "ideas", + "test", + "review", + "platform" + ] + }, + { + "login": "GargantuaX", + "name": "GargantuaX", + "avatar_url": "https://avatars.githubusercontent.com/u/14013111?v=4", + "profile": "https://github.com/GargantuaX", + "contributions": [ + "financial" + ] + }, + { + "login": "Igogrek", + "name": "Igor Minin", + "avatar_url": "https://avatars.githubusercontent.com/u/12101721?v=4", + "profile": "https://bednya.ga/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "jaesung9507", + "name": "Jae-Sung Lee", + "avatar_url": "https://avatars.githubusercontent.com/u/39658806?v=4", + "profile": "https://www.jae-sung.com/", + "contributions": [ + "code", + "ideas" + ] + }, + { + "login": "Jarek-SRT", + "name": "Jarek", + "avatar_url": "https://avatars.githubusercontent.com/u/3391365?v=4", + "profile": "https://github.com/Jarek-SRT", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "Junkher", + "name": "Junker", + "avatar_url": "https://avatars.githubusercontent.com/u/85776620?v=4", + "profile": "https://github.com/Junkher", + "contributions": [ + "doc" + ] + }, + { + "login": "kraney", + "name": "Kris Raney", + "avatar_url": "https://avatars.githubusercontent.com/u/5760081?v=4", + "profile": "https://github.com/kraney", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "LukenSkyne", + "name": "Luken", + "avatar_url": "https://avatars.githubusercontent.com/u/29918069?v=4", + "profile": "https://github.com/LukenSkyne", + "contributions": [ + "doc" + ] + }, + { + "login": "ocelotsloth", + "name": "Mark Stenglein", + "avatar_url": "https://avatars.githubusercontent.com/u/9255772?v=4", + "profile": "https://markstenglein.com/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "buddyabaddon", + "name": "buddyabaddon", + "avatar_url": "https://avatars.githubusercontent.com/u/33861511?v=4", + "profile": "https://github.com/buddyabaddon", + "contributions": [ + "code" + ] + }, + { + "login": "MikeSchaap", + "name": "MikeSchaap", + "avatar_url": "https://avatars.githubusercontent.com/u/35368821?v=4", + "profile": "https://github.com/MikeSchaap", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "Orijhins", + "name": "NYSSEN Michaël", + "avatar_url": "https://avatars.githubusercontent.com/u/47521598?v=4", + "profile": "https://github.com/Orijhins", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "NanoNik", + "name": "Nan0", + "avatar_url": "https://avatars.githubusercontent.com/u/11991329?v=4", + "profile": "https://github.com/NanoNik", + "contributions": [ + "code", + "ideas", + "test", + "review" + ] + }, + { + "login": "marcio199226", + "name": "oskar", + "avatar_url": "https://avatars.githubusercontent.com/u/10244404?v=4", + "profile": "https://github.com/marcio199226", + "contributions": [ + "doc" + ] + }, + { + "login": "pierrejoye", + "name": "Pierre Joye", + "avatar_url": "https://avatars.githubusercontent.com/u/282408?v=4", + "profile": "https://github.com/pierrejoye", + "contributions": [ + "code", + "bug", + "ideas", + "test" + ] + }, + { + "login": "Rested", + "name": "Reuben Thomas-Davis", + "avatar_url": "https://avatars.githubusercontent.com/u/2003608?v=4", + "profile": "https://github.com/Rested", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "mewmew", + "name": "Robin", + "avatar_url": "https://avatars.githubusercontent.com/u/1414531?v=4", + "profile": "https://github.com/mewmew", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "sebastian0x62", + "name": "Sebastian Bauer", + "avatar_url": "https://avatars.githubusercontent.com/u/70367451?v=4", + "profile": "https://threema.id/YSB3TVF7", + "contributions": [ + "code", + "ideas", + "test", + "review", + "question" + ] + }, + { + "login": "sidwebworks", + "name": "Sidharth Rathi", + "avatar_url": "https://avatars.githubusercontent.com/u/58144379?v=4", + "profile": "https://github.com/sidwebworks", + "contributions": [ + "doc", + "bug" + ] + }, + { + "login": "sithembiso", + "name": "Sithembiso Khumalo", + "avatar_url": "https://avatars.githubusercontent.com/u/6559905?v=4", + "profile": "https://github.com/sithembiso", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "LanguageAgnostic", + "name": "Soheib El-Harrache", + "avatar_url": "https://avatars.githubusercontent.com/u/19310562?v=4", + "profile": "https://github.com/LanguageAgnostic", + "contributions": [ + "code", + "bug", + "financial" + ] + }, + { + "login": "SophieAu", + "name": "Sophie Au", + "avatar_url": "https://avatars.githubusercontent.com/u/11145039?v=4", + "profile": "https://www.sophieau.com/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "stefpap", + "name": "Stefanos Papadakis", + "avatar_url": "https://avatars.githubusercontent.com/u/22637722?v=4", + "profile": "https://github.com/stefpap", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "s12chung", + "name": "Steve Chung", + "avatar_url": "https://avatars.githubusercontent.com/u/263394?v=4", + "profile": "https://github.com/s12chung", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "TAINCER", + "name": "Timm Ortloff", + "avatar_url": "https://avatars.githubusercontent.com/u/41272726?v=4", + "profile": "https://tortloff.de/", + "contributions": [ + "doc" + ] + }, + { + "login": "tomanagle", + "name": "Tom", + "avatar_url": "https://avatars.githubusercontent.com/u/8683577?v=4", + "profile": "https://github.com/tomanagle", + "contributions": [ + "code" + ] + }, + { + "login": "ValentinTrinque", + "name": "Valentin Trinqué", + "avatar_url": "https://avatars.githubusercontent.com/u/4662842?v=4", + "profile": "https://www.linkedin.com/in/valentintrinque", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "mattn", + "name": "mattn", + "avatar_url": "https://avatars.githubusercontent.com/u/10111?v=4", + "profile": "https://mattn.kaoriya.net/", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "bearsh", + "name": "bearsh", + "avatar_url": "https://avatars.githubusercontent.com/u/1089356?v=4", + "profile": "https://github.com/bearsh", + "contributions": [ + "code", + "ideas", + "doc" + ] + }, + { + "login": "chenxiao1990", + "name": "chenxiao", + "avatar_url": "https://avatars.githubusercontent.com/u/16933565?v=4", + "profile": "https://github.com/chenxiao1990", + "contributions": [ + "code", + "ideas", + "doc" + ] + }, + { + "login": "fengweiqiang", + "name": "fengweiqiang", + "avatar_url": "https://avatars.githubusercontent.com/u/22905300?v=4", + "profile": "https://github.com/fengweiqiang", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "flin7", + "name": "flin7", + "avatar_url": "https://avatars.githubusercontent.com/u/58138185?v=4", + "profile": "https://github.com/flin7", + "contributions": [ + "doc" + ] + }, + { + "login": "fred21O4", + "name": "fred21O4", + "avatar_url": "https://avatars.githubusercontent.com/u/67189813?v=4", + "profile": "https://github.com/fred21O4", + "contributions": [ + "doc" + ] + }, + { + "login": "gardc", + "name": "gardc", + "avatar_url": "https://avatars.githubusercontent.com/u/41453409?v=4", + "profile": "https://github.com/gardc", + "contributions": [ + "doc", + "tutorial" + ] + }, + { + "login": "rayshoo", + "name": "rayshoo", + "avatar_url": "https://avatars.githubusercontent.com/u/52561899?v=4", + "profile": "https://github.com/rayshoo", + "contributions": [ + "doc" + ] + }, + { + "login": "Yz4230", + "name": "Ishiyama Yuzuki", + "avatar_url": "https://avatars.githubusercontent.com/u/38999742?v=4", + "profile": "https://github.com/Yz4230", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "Baiyuetribe", + "name": "佰阅", + "avatar_url": "https://avatars.githubusercontent.com/u/43716063?v=4", + "profile": "https://baiyue.one/", + "contributions": [ + "code" + ] + }, + { + "login": "daodao97", + "name": "刀刀", + "avatar_url": "https://avatars.githubusercontent.com/u/15009280?v=4", + "profile": "https://github.com/daodao97", + "contributions": [ + "doc", + "bug" + ] + }, + { + "login": "jicg", + "name": "归位", + "avatar_url": "https://avatars.githubusercontent.com/u/6479672?v=4", + "profile": "https://github.com/jicg", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "skamensky", + "name": "skamensky", + "avatar_url": "https://avatars.githubusercontent.com/u/19151369?v=4", + "profile": "https://github.com/skamensky", + "contributions": [ + "code", + "ideas", + "doc" + ] + }, + { + "login": "dependabot[bot]", + "name": "dependabot[bot]", + "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", + "profile": "https://github.com/apps/dependabot", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "dsieradzki", + "name": "Damian Sieradzki", + "avatar_url": "https://avatars.githubusercontent.com/u/10297559?v=4", + "profile": "https://www.linkedin.com/in/dsieradzki/", + "contributions": [ + "financial" + ] + }, + { + "login": "boostchicken", + "name": "John Dorman", + "avatar_url": "https://avatars.githubusercontent.com/u/427295?v=4", + "profile": "https://github.com/boostchicken", + "contributions": [ + "financial" + ] + }, + { + "login": "iansinnott", + "name": "Ian Sinnott", + "avatar_url": "https://avatars.githubusercontent.com/u/3154865?v=4", + "profile": "https://blog.iansinnott.com/", + "contributions": [ + "financial" + ] + }, + { + "login": "Shackelford-Arden", + "name": "Arden Shackelford", + "avatar_url": "https://avatars.githubusercontent.com/u/7362263?v=4", + "profile": "https://github.com/Shackelford-Arden", + "contributions": [ + "financial" + ] + }, + { + "login": "Bironou", + "name": "Bironou", + "avatar_url": "https://avatars.githubusercontent.com/u/107761511?v=4", + "profile": "https://github.com/Bironou", + "contributions": [ + "financial" + ] + }, + { + "login": "CharlieGo19", + "name": "CharlieGo_", + "avatar_url": "https://avatars.githubusercontent.com/u/62405980?v=4", + "profile": "https://github.com/CharlieGo19", + "contributions": [ + "financial" + ] + }, + { + "login": "overnet", + "name": "overnet", + "avatar_url": "https://avatars.githubusercontent.com/u/6376126?v=4", + "profile": "https://github.com/overnet", + "contributions": [ + "financial" + ] + }, + { + "login": "jugglingjsons", + "name": "jugglingjsons", + "avatar_url": "https://avatars.githubusercontent.com/u/20739064?v=4", + "profile": "https://jugglingjsons.dev/", + "contributions": [ + "financial" + ] + }, + { + "login": "selvindev", + "name": "Selvin Ortiz", + "avatar_url": "https://avatars.githubusercontent.com/u/1922523?v=4", + "profile": "https://selvin.dev/", + "contributions": [ + "financial" + ] + }, + { + "login": "zandercodes", + "name": "ZanderCodes", + "avatar_url": "https://avatars.githubusercontent.com/u/46308805?v=4", + "profile": "https://github.com/zandercodes", + "contributions": [ + "financial" + ] + }, + { + "login": "DonTomato", + "name": "Michael Voronov", + "avatar_url": "https://avatars.githubusercontent.com/u/1098084?v=4", + "profile": "https://github.com/DonTomato", + "contributions": [ + "financial" + ] + }, + { + "login": "letheanVPN", + "name": "letheanVPN", + "avatar_url": "https://avatars.githubusercontent.com/u/83868036?v=4", + "profile": "https://lt.hn/", + "contributions": [ + "financial" + ] + }, + { + "login": "taigrr", + "name": "Tai Groot", + "avatar_url": "https://avatars.githubusercontent.com/u/8261498?v=4", + "profile": "https://taigrr.com/", + "contributions": [ + "financial" + ] + }, + { + "login": "easy-web-it", + "name": "easy-web-it", + "avatar_url": "https://avatars.githubusercontent.com/u/95484991?v=4", + "profile": "https://github.com/easy-web-it", + "contributions": [ + "financial" + ] + }, + { + "login": "michaelolson1996", + "name": "Michael Olson", + "avatar_url": "https://avatars.githubusercontent.com/u/45323107?v=4", + "profile": "https://michaelolson1996.github.io/portfolio", + "contributions": [ + "financial" + ] + }, + { + "login": "EdenNetworkItalia", + "name": "EdenNetwork Italia", + "avatar_url": "https://avatars.githubusercontent.com/u/4912777?v=4", + "profile": "https://eden.network/", + "contributions": [ + "financial" + ] + }, + { + "login": "ondoki", + "name": "ondoki", + "avatar_url": "https://avatars.githubusercontent.com/u/88536792?v=4", + "profile": "https://github.com/ondoki", + "contributions": [ + "financial" + ] + }, + { + "login": "questrail", + "name": "QuEST Rail LLC", + "avatar_url": "https://avatars.githubusercontent.com/u/3536569?v=4", + "profile": "https://github.com/questrail", + "contributions": [ + "financial" + ] + }, + { + "login": "Gilgames000", + "name": "Gilgameš", + "avatar_url": "https://avatars.githubusercontent.com/u/22778436?v=4", + "profile": "https://github.com/Gilgames000", + "contributions": [ + "financial" + ] + }, + { + "login": "bbergshaven", + "name": "Bernt-Johan Bergshaven", + "avatar_url": "https://avatars.githubusercontent.com/u/4091634?v=4", + "profile": "https://github.com/bbergshaven", + "contributions": [ + "financial" + ] + }, + { + "login": "bglw", + "name": "Liam Bigelow", + "avatar_url": "https://avatars.githubusercontent.com/u/40188355?v=4", + "profile": "https://github.com/bglw", + "contributions": [ + "financial" + ] + }, + { + "login": "nickarellano", + "name": "Nick Arellano", + "avatar_url": "https://avatars.githubusercontent.com/u/13930605?v=4", + "profile": "https://github.com/nickarellano", + "contributions": [ + "financial" + ] + }, + { + "login": "fcjr", + "name": "Frank Chiarulli Jr.", + "avatar_url": "https://avatars.githubusercontent.com/u/2053002?v=4", + "profile": "https://github.com/fcjr", + "contributions": [ + "financial" + ] + }, + { + "login": "tylertravisty", + "name": "Tyler", + "avatar_url": "https://avatars.githubusercontent.com/u/8620352?v=4", + "profile": "https://github.com/tylertravisty", + "contributions": [ + "financial" + ] + }, + { + "login": "trea", + "name": "Trea Hauet", + "avatar_url": "https://avatars.githubusercontent.com/u/1181448?v=4", + "profile": "https://github.com/trea", + "contributions": [ + "financial" + ] + }, + { + "login": "picatz", + "name": "Kent 'picat' Gruber", + "avatar_url": "https://avatars.githubusercontent.com/u/14850816?v=4", + "profile": "https://picatz.github.io/", + "contributions": [ + "financial" + ] + }, + { + "login": "tc-hib", + "name": "tc-hib", + "avatar_url": "https://avatars.githubusercontent.com/u/55949036?v=4", + "profile": "https://github.com/tc-hib", + "contributions": [ + "financial" + ] + }, + { + "login": "acheong08", + "name": "Antonio", + "avatar_url": "https://avatars.githubusercontent.com/u/36258159?v=4", + "profile": "https://github.com/acheong08", + "contributions": [ + "doc" + ] + }, + { + "login": "MyNameIsAres", + "name": "MyNameIsAres", + "avatar_url": "https://avatars.githubusercontent.com/u/32432637?v=4", + "profile": "https://github.com/MyNameIsAres", + "contributions": [ + "doc" + ] + }, + { + "login": "Maicarons2022", + "name": "Maicarons J", + "avatar_url": "https://avatars.githubusercontent.com/u/101958587?v=4", + "profile": "http://mai.car.ons", + "contributions": [ + "doc" + ] + }, + { + "login": "KiddoV", + "name": "kiddov", + "avatar_url": "https://avatars.githubusercontent.com/u/28552977?v=4", + "profile": "https://github.com/KiddoV", + "contributions": [ + "doc", + "financial", + "test", + "ideas" + ] + }, + { + "login": "Ilshidur", + "name": "Nicolas Coutin", + "avatar_url": "https://avatars.githubusercontent.com/u/6564012?v=4", + "profile": "https://nicolas-coutin.com/", + "contributions": [ + "financial" + ] + }, + { + "login": "ParvinEyvazov", + "name": "Parvin Eyvazov", + "avatar_url": "https://avatars.githubusercontent.com/u/32189770?v=4", + "profile": "https://github.com/ParvinEyvazov", + "contributions": [ + "doc" + ] + }, + { + "login": "github-actions[bot]", + "name": "github-actions[bot]", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "profile": "https://github.com/apps/github-actions", + "contributions": [ + "code" + ] + }, + { + "login": "OlegGulevskyy", + "name": "Oleg Gulevskyy", + "avatar_url": "https://avatars.githubusercontent.com/u/43781031?v=4", + "profile": "https://github.com/OlegGulevskyy", + "contributions": [ + "code", + "doc", + "maintenance", + "platform" + ] + }, + { + "login": "raguay", + "name": "Richard Guay", + "avatar_url": "https://avatars.githubusercontent.com/u/2487495?v=4", + "profile": "http://www.customct.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "ATenderholt", + "name": "Adam Tenderholt", + "avatar_url": "https://avatars.githubusercontent.com/u/740623?v=4", + "profile": "https://github.com/ATenderholt", + "contributions": [ + "code" + ] + }, + { + "login": "JulioDRF", + "name": "JulioDRF", + "avatar_url": "https://avatars.githubusercontent.com/u/15677708?v=4", + "profile": "https://github.com/JulioDRF", + "contributions": [ + "code" + ] + }, + { + "login": "scottopell", + "name": "Scott Opell", + "avatar_url": "https://avatars.githubusercontent.com/u/996472?v=4", + "profile": "http://scottopell.com/", + "contributions": [ + "code" + ] + }, + { + "login": "avengerweb", + "name": "Vadim Shchepotev", + "avatar_url": "https://avatars.githubusercontent.com/u/2055581?v=4", + "profile": "https://aven.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "willdot", + "name": "Will Andrews", + "avatar_url": "https://avatars.githubusercontent.com/u/4906530?v=4", + "profile": "https://willdot.net/", + "contributions": [ + "code" + ] + }, + { + "login": "gwynforthewyn", + "name": "Gwyn", + "avatar_url": "https://avatars.githubusercontent.com/u/434656?v=4", + "profile": "https://github.com/gwynforthewyn", + "contributions": [ + "code", + "review", + "question", + "research" + ] + }, + { + "login": "xijaja", + "name": "希嘉嘉", + "avatar_url": "https://avatars.githubusercontent.com/u/47017666?v=4", + "profile": "https://github.com/xijaja", + "contributions": [ + "code" + ] + }, + { + "login": "almas1992", + "name": "ALMAS", + "avatar_url": "https://avatars.githubusercontent.com/u/9382335?v=4", + "profile": "https://www.almas.cc/", + "contributions": [ + "code" + ] + }, + { + "login": "o8x", + "name": "Alex", + "avatar_url": "https://avatars.githubusercontent.com/u/20666153?v=4", + "profile": "https://stdout.com.cn/", + "contributions": [ + "code" + ] + }, + { + "login": "arifali123", + "name": "Arif Ali", + "avatar_url": "https://avatars.githubusercontent.com/u/51419655?v=4", + "profile": "https://github.com/arifali123", + "contributions": [ + "code" + ] + }, + { + "login": "hotafrika", + "name": "Artur Siarohau", + "avatar_url": "https://avatars.githubusercontent.com/u/18332839?v=4", + "profile": "https://github.com/hotafrika", + "contributions": [ + "code" + ] + }, + { + "login": "binyamin", + "name": "Binyamin Aron Green", + "avatar_url": "https://avatars.githubusercontent.com/u/39805353?v=4", + "profile": "https://binyam.in/", + "contributions": [ + "code" + ] + }, + { + "login": "bdwyertech", + "name": "Brian Dwyer", + "avatar_url": "https://avatars.githubusercontent.com/u/2973273?v=4", + "profile": "http://bdwyertech.net/", + "contributions": [ + "code" + ] + }, + { + "login": "ckilb", + "name": "Christian Kilb", + "avatar_url": "https://avatars.githubusercontent.com/u/7283097?v=4", + "profile": "http://www.cilb.de/", + "contributions": [ + "code" + ] + }, + { + "login": "edwargix", + "name": "David Florness", + "avatar_url": "https://avatars.githubusercontent.com/u/22877007?v=4", + "profile": "https://github.com/edwargix", + "contributions": [ + "doc" + ] + }, + { + "login": "BuckeyeCoder", + "name": "David Walton", + "avatar_url": "https://avatars.githubusercontent.com/u/95933880?v=4", + "profile": "https://github.com/BuckeyeCoder", + "contributions": [ + "code" + ] + }, + { + "login": "Debdut", + "name": "Debdut Karmakar", + "avatar_url": "https://avatars.githubusercontent.com/u/7561070?v=4", + "profile": "https://github.com/Debdut", + "contributions": [ + "code" + ] + }, + { + "login": "gotid", + "name": "Dieter Zhu", + "avatar_url": "https://avatars.githubusercontent.com/u/4010854?v=4", + "profile": "https://github.com/gotid", + "contributions": [ + "code" + ] + }, + { + "login": "Holmqvist1990", + "name": "Fredrik Holmqvist", + "avatar_url": "https://avatars.githubusercontent.com/u/22743750?v=4", + "profile": "https://fredrikholmqvist.com/", + "contributions": [ + "code" + ] + }, + { + "login": "giopalma", + "name": "Giovanni Palma", + "avatar_url": "https://avatars.githubusercontent.com/u/33783684?v=4", + "profile": "https://github.com/giopalma", + "contributions": [ + "code" + ] + }, + { + "login": "Nexus26404", + "name": "Hao", + "avatar_url": "https://avatars.githubusercontent.com/u/83110373?v=4", + "profile": "https://github.com/Nexus26404", + "contributions": [ + "code" + ] + }, + { + "login": "i7tsov", + "name": "Igor Sementsov", + "avatar_url": "https://avatars.githubusercontent.com/u/44977153?v=4", + "profile": "https://github.com/i7tsov", + "contributions": [ + "code" + ] + }, + { + "login": "derhasi", + "name": "Johannes Haseitl", + "avatar_url": "https://avatars.githubusercontent.com/u/118502?v=4", + "profile": "https://github.com/derhasi", + "contributions": [ + "code" + ] + }, + { + "login": "joshbuddy", + "name": "Joshua Hull", + "avatar_url": "https://avatars.githubusercontent.com/u/8898?v=4", + "profile": "https://github.com/joshbuddy", + "contributions": [ + "code" + ] + }, + { + "login": "joshm998", + "name": "Joshua Mangiola", + "avatar_url": "https://avatars.githubusercontent.com/u/1779737?v=4", + "profile": "https://github.com/joshm998", + "contributions": [ + "doc" + ] + }, + { + "login": "prurigro", + "name": "Kevin MacMartin", + "avatar_url": "https://avatars.githubusercontent.com/u/1149238?v=4", + "profile": "https://github.com/prurigro", + "contributions": [ + "code" + ] + }, + { + "login": "liang-li-dev", + "name": "Liang Li", + "avatar_url": "https://avatars.githubusercontent.com/u/112530363?v=4", + "profile": "https://github.com/liang-li-dev", + "contributions": [ + "code" + ] + }, + { + "login": "marvinhosea", + "name": "Marvin Collins Hosea", + "avatar_url": "https://avatars.githubusercontent.com/u/7722584?v=4", + "profile": "https://appslab.co.ke/", + "contributions": [ + "code" + ] + }, + { + "login": "mholt", + "name": "Matt Holt", + "avatar_url": "https://avatars.githubusercontent.com/u/1128849?v=4", + "profile": "https://matt.life/", + "contributions": [ + "code" + ] + }, + { + "login": "Gurkengewuerz", + "name": "Niklas", + "avatar_url": "https://avatars.githubusercontent.com/u/10966337?v=4", + "profile": "https://github.com/Gurkengewuerz", + "contributions": [ + "code" + ] + }, + { + "login": "Xhofe", + "name": "Andy Hsu", + "avatar_url": "https://avatars.githubusercontent.com/u/36558727?v=4", + "profile": "https://github.com/Xhofe", + "contributions": [ + "code" + ] + }, + { + "login": "NullCode1337", + "name": "NullCode", + "avatar_url": "https://avatars.githubusercontent.com/u/70959549?v=4", + "profile": "https://github.com/NullCode1337", + "contributions": [ + "code" + ] + }, + { + "login": "oSethoum", + "name": "Oussama Sethoum", + "avatar_url": "https://avatars.githubusercontent.com/u/88779394?v=4", + "profile": "https://github.com/oSethoum", + "contributions": [ + "code" + ] + }, + { + "login": "ParkourLiu", + "name": "ParkourLiu", + "avatar_url": "https://avatars.githubusercontent.com/u/33681340?v=4", + "profile": "https://github.com/ParkourLiu", + "contributions": [ + "code" + ] + }, + { + "login": "zllovesuki", + "name": "Rachel Chen", + "avatar_url": "https://avatars.githubusercontent.com/u/298453?v=4", + "profile": "https://github.com/zllovesuki", + "contributions": [ + "code" + ] + }, + { + "login": "rnice01", + "name": "Rob Nice", + "avatar_url": "https://avatars.githubusercontent.com/u/11394384?v=4", + "profile": "https://github.com/rnice01", + "contributions": [ + "code" + ] + }, + { + "login": "RyoTagami", + "name": "Ryo TAGAMI", + "avatar_url": "https://avatars.githubusercontent.com/u/9672589?v=4", + "profile": "https://github.com/RyoTagami", + "contributions": [ + "code" + ] + }, + { + "login": "SamHennessy", + "name": "Sam Hennessy", + "avatar_url": "https://avatars.githubusercontent.com/u/119867?v=4", + "profile": "https://github.com/SamHennessy", + "contributions": [ + "code" + ] + }, + { + "login": "AlbinoDrought", + "name": "Sean", + "avatar_url": "https://avatars.githubusercontent.com/u/852873?v=4", + "profile": "https://albinodrought.com/", + "contributions": [ + "code" + ] + }, + { + "login": "sgosiaco", + "name": "Sean Gosiaco", + "avatar_url": "https://avatars.githubusercontent.com/u/212341?v=4", + "profile": "https://github.com/sgosiaco", + "contributions": [ + "code" + ] + }, + { + "login": "SheetJSDev", + "name": "Eric P Sheets", + "avatar_url": "https://avatars.githubusercontent.com/u/6070939?v=4", + "profile": "https://sheetjs.com/", + "contributions": [ + "code" + ] + }, + { + "login": "SupianIDz", + "name": "Supian M", + "avatar_url": "https://avatars.githubusercontent.com/u/37969970?v=4", + "profile": "https://www.octopy.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "Watson-Sei", + "name": "Watson-Sei", + "avatar_url": "https://avatars.githubusercontent.com/u/55475145?v=4", + "profile": "https://github.com/Watson-Sei", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "shinshin86", + "name": "Yuki Shindo", + "avatar_url": "https://avatars.githubusercontent.com/u/8216064?v=4", + "profile": "https://shinshin86.com/", + "contributions": [ + "code" + ] + }, + { + "login": "cuigege", + "name": "cuigege", + "avatar_url": "https://avatars.githubusercontent.com/u/26080122?v=4", + "profile": "https://github.com/cuigege", + "contributions": [ + "code" + ] + }, + { + "login": "cybertramp", + "name": "cybertramp", + "avatar_url": "https://avatars.githubusercontent.com/u/30935096?v=4", + "profile": "https://cybertramp.net/", + "contributions": [ + "code" + ] + }, + { + "login": "h8gi", + "name": "hiroki yagi", + "avatar_url": "https://avatars.githubusercontent.com/u/10811057?v=4", + "profile": "https://github.com/h8gi", + "contributions": [ + "code" + ] + }, + { + "login": "imgbot[bot]", + "name": "imgbot[bot]", + "avatar_url": "https://avatars.githubusercontent.com/in/4706?v=4", + "profile": "https://github.com/apps/imgbot", + "contributions": [ + "code" + ] + }, + { + "login": "tong3jie", + "name": "juju", + "avatar_url": "https://avatars.githubusercontent.com/u/14191774?v=4", + "profile": "https://github.com/tong3jie", + "contributions": [ + "code" + ] + }, + { + "login": "meatherly", + "name": "Michael Eatherly", + "avatar_url": "https://avatars.githubusercontent.com/u/1327960?v=4", + "profile": "http://meatherly.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "tk103331", + "name": "tk", + "avatar_url": "https://avatars.githubusercontent.com/u/4404609?v=4", + "profile": "https://github.com/tk103331", + "contributions": [ + "code" + ] + }, + { + "login": "allcontributors[bot]", + "name": "allcontributors[bot]", + "avatar_url": "https://avatars.githubusercontent.com/in/23186?v=4", + "profile": "https://github.com/apps/allcontributors", + "contributions": [ + "doc" + ] + }, + { + "login": "wandercn", + "name": "wander", + "avatar_url": "https://avatars.githubusercontent.com/u/77320953?v=4", + "profile": "https://www.ffactory.org/", + "contributions": [ + "doc" + ] + } + ], + "contributorsPerLine": 8, + "projectName": "wails", + "projectOwner": "wailsapp", + "repoType": "github", + "repoHost": "https://github.com", + "skipCi": true, + "commitConvention": "none" +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..f1f9371d2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ leaanthony ] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 2817de1fa..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Description** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behaviour: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behaviour** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**System Details** -Please paste the output of `wails report` here. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..9faf71704 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,87 @@ +name: Bug Report +description: Create a report to help us improve +# title: "" +labels: ["Bug"] + +body: + - type: markdown + attributes: + value: | + ***Please note: No bug reports are currently being accepted for Wails v3*** + Before submitting this issue, please do the following: + - Do a web search for your error. This usually leads to a much better understanding of the issue. + - Prove that the error is indeed a Wails bug and not an application bug, with a specific set of steps to reproduce. + - Search the issue tracker using [this link](https://github.com/wailsapp/wails/issues?q=is%3Aissue+). + - Search the [discussion forums](https://github.com/wailsapp/wails/discussions?discussions_q=type+your+issue+here). + - Read the [Troubleshooting Guide](https://wails.io/docs/next/guides/troubleshooting). + - Create a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) and link to it in the issue. + - If your issue is related to TypeScript generation, please open a ticket and create a PR with a failing test case. + TS tests can be found [here](https://github.com/wailsapp/wails/tree/master/v2/internal/binding/binding_test). Remember to add + your test to the `binding_test.go` file. + - Try to fix it yourself. Keep a list of things you have done to fix the problem. + + If after doing all the above, the problem remains, please continue with this ticket providing *all* the information requested. + + Bug reports that do not follow these steps will likely be closed, so please help us to help you. + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + placeholder: A clear and concise description of what the bug is. + # value: "" + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: Steps to reproduce the behaviour + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: expected-behaviour + attributes: + label: Expected behaviour + description: A clear and concise description of what you expected to happen. + placeholder: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + placeholder: If applicable, add screenshots to help explain your problem. + validations: + required: false + - type: textarea + id: attempted-fixes + attributes: + label: Attempted Fixes + description: A list of things you have tried to fix the problem, including search engine links. + placeholder: A list of things you have tried to fix the problem, including search engine links. + validations: + required: false + - type: textarea + id: systemdetails + attributes: + label: System Details + description: Please add the output of `wails doctor`. + render: shell + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. + placeholder: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..e3632ecaa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: true +contact_links: + - name: Discord Chat + url: https://discord.gg/BrRSWTaxVK + about: Ask questions and discuss with other Wails users in real time. + + - name: GitHub Community Discussions + url: https://github.com/wailsapp/wails/discussions + about: If your question is not a feature or a bug, please go to the discussion panel and retrieve if your question already exists before submitting. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 000000000..c1259587b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,40 @@ +name: Documentation +description: Report an issue related to documentation. +# title: "" +labels: ["Documentation"] + +body: + - type: markdown + attributes: + value: | + This template is only used for documentation related requests, including: + + - Log undocumented APIs + - Update link + - Documentation other than non-project code + - Add new language + + If you followed the documentation but things don't work, take some time to consider if it's the documentation or the code that's wrong. In the latter, prefer using the "[Bug Report](https://github.com/wailsapp/wails/issues/new?assignees=&labels=bug&template=bug_report.yml)" template. + + - type: checkboxes + attributes: + label: Have you read the Documentation Contribution Guidelines? + options: + - label: I have read the [Documentation Contribution Guidelines](https://wails.io/community-guide#ways-of-contributing). + required: true + + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the issue is. + validations: + required: true + + - type: checkboxes + attributes: + label: Self-service + description: | + If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. + If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. + options: + - label: I'd be willing to address this documentation request myself. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..b346c0bdc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest an idea for this project +# title: "" +labels: ["Enhancement"] + +body: + - type: markdown + attributes: + value: | + Before opening a feature request, please check the [Roadmap](https://github.com/wailsapp/wails/discussions/1484) to see if it has already been requested. + ***Please note: No new feature requests are being accepted for Wails v1*** + + - type: textarea + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: false + + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false \ No newline at end of file diff --git a/.github/file-labeler.yml b/.github/file-labeler.yml new file mode 100644 index 000000000..69494cbae --- /dev/null +++ b/.github/file-labeler.yml @@ -0,0 +1,44 @@ +# File path specific labels +v2-only: + - 'v2/**/*' + +v3-alpha: + - 'v3/**/*' + +windows: + - '**/*_windows.go' + - 'v2/internal/frontend/desktop/windows/**/*' + +macos: + - '**/*_darwin.go' + - 'v2/internal/frontend/desktop/darwin/**/*' + +linux: + - '**/*_linux.go' + - 'v2/internal/frontend/desktop/linux/**/*' + +cli: + - 'v2/cmd/**/*' + - 'v3/cmd/**/*' + - '**/cli/**/*' + - '**/commands/**/*' + +documentation: + - '**/*.md' + - 'docs/**/*' + - 'website/**/*' + - 'mkdocs-website/**/*' + +templates: + - '**/templates/**/*' + - '**/template/**/*' + +runtime: + - '**/runtime/**/*' + - 'v2/internal/runtime/**/*' + - 'v3/internal/runtime/**/*' + +bindings: + - 'v2/internal/binding/**/*' + - 'v3/internal/generator/**/*' + diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml new file mode 100644 index 000000000..0a7949051 --- /dev/null +++ b/.github/issue-labeler.yml @@ -0,0 +1,144 @@ +# Version labels +v2-only: + - '\[v2\]' + - '\(v2\)' + - 'v2:' + - 'version 2' + - 'wails v2' + - 'using v2' + - 'master branch' + +v3-alpha: + - '\[v3\]' + - '\(v3\)' + - 'v3:' + - '\[v3-alpha\]' + - '\(v3-alpha\)' + - 'version 3' + - 'wails v3' + - 'using v3' + - 'v3-alpha branch' + +# Component labels +webview2: + - 'webview2' + - 'windows' + - 'microsoft edge' + - 'edge browser' + - 'IE' + - 'Explorer' + - 'browser crashes' + +macos: + - 'macOS' + - 'mac OS' + - 'OS X' + - 'darwin' + - 'cocoa' + - 'Safari' + - 'Catalyst' + - 'Ventura' + - 'Sonoma' + - 'apple' + +linux: + - 'linux' + - 'ubuntu' + - 'debian' + - 'fedora' + - 'gtk' + - 'webkitgtk' + - 'webkit2gtk' + - 'gnome' + - 'x11' + - 'wayland' + +cli: + - 'cli' + - 'command line' + - 'wails doctor' + - 'wails init' + - 'wails build' + - 'wails dev' + - 'template' + - 'scaffolding' + +# Type labels +bug: + - 'bug' + - 'crash' + - 'broken' + - 'failure' + - 'error' + - 'failed' + - 'panic' + - 'segfault' + - 'issue' + - 'not working' + - 'problem' + +enhancement: + - 'feature' + - 'enhancement' + - 'request' + - 'add' + - 'new' + - 'improve' + - 'functionality' + - 'support for' + - 'please add' + - 'would be nice' + +documentation: + - 'docs' + - 'documentation' + - 'readme' + - 'example' + - 'tutorial' + - 'guide' + - 'explanation' + - 'clarification' + - 'instructions' + +security: + - 'security' + - 'vulnerability' + - 'exploit' + - 'hack' + - 'CVE' + - 'secure' + - 'encryption' + - 'hardening' + +performance: + - 'performance' + - 'slow' + - 'speed' + - 'memory leak' + - 'cpu usage' + - 'high memory' + - 'lag' + - 'freeze' + - 'optimization' + +# Priority labels +high-priority: + - 'urgent' + - 'critical' + - 'security' + - 'high priority' + - 'important' + - 'production' + - 'blocker' + - 'blocking' + +question: + - 'how to' + - 'how do i' + - 'can I' + - 'is it possible' + - 'question' + - 'help me' + - 'need help' + - 'assistance' + - 'confused' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..d73efffa8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,57 @@ + + + +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please select the option that is relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration using `wails doctor`. + +- [ ] Windows +- [ ] macOS +- [ ] Linux + +If you checked Linux, please specify the distro and version. + +## Test Configuration + +Please paste the output of `wails doctor`. If you are unable to run this command, please describe your environment in as much detail as possible. + +# Checklist: + +- [ ] I have updated `website/src/pages/changelog.mdx` with details of this PR +- [ ] My code follows the general coding style of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..d8bcc83ec --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,36 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 45 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 10 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - onhold + - inprogress + - "Selected For Development" + - bug + - enhancement + - v3-alpha + - high-priority +# Label to use when marking an issue as stale +staleLabel: "stale" +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs within the next 10 days. + + If this issue is still relevant, please add a comment to keep it open. + Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue has been automatically closed due to lack of activity. + Please feel free to reopen it if it's still relevant. +exemptMilestones: true +exemptAssignees: true +# Only mark issues (not PRs) +only: issues +# Exempt issues created before a certain date +exemptCreatedBefore: "2024-01-01T00:00:00Z" +# Starts checking issues only after the specified date +startDate: "2025-06-01T00:00:00Z" diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml new file mode 100644 index 000000000..3d7a86450 --- /dev/null +++ b/.github/workflows/auto-label-issues.yml @@ -0,0 +1,33 @@ +name: Auto Label Issues + +on: + issues: + types: [opened, edited, reopened] + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + auto-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Label issues and PRs by content + uses: github/issue-labeler@v3.4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/issue-labeler.yml + enable-versioned-regex: 0 + include-title: 1 + + - name: Label issues and PRs by file paths + uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/file-labeler.yml + sync-labels: true diff --git a/.github/workflows/build-and-test-v3.yml b/.github/workflows/build-and-test-v3.yml new file mode 100644 index 000000000..bfcef85a3 --- /dev/null +++ b/.github/workflows/build-and-test-v3.yml @@ -0,0 +1,201 @@ +name: Build + Test v3 + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - v3-alpha + paths: + - 'v3/**' + pull_request_review: + types: [submitted] + branches: + - v3-alpha + +jobs: + check_approval: + name: Check PR Approval + runs-on: ubuntu-latest + if: github.base_ref == 'v3-alpha' + outputs: + approved: ${{ steps.check.outputs.approved }} + steps: + - name: Check if PR is approved + id: check + run: | + if [[ "${{ github.event.review.state }}" == "approved" || "${{ github.event.pull_request.approved }}" == "true" ]]; then + echo "approved=true" >> $GITHUB_OUTPUT + else + echo "approved=false" >> $GITHUB_OUTPUT + fi + + test_go: + name: Run Go Tests v3 + needs: check_approval + runs-on: ${{ matrix.os }} + if: github.base_ref == 'v3-alpha' + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + go-version: [1.24] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install linux dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-latest' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk + version: 1.0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: "v3/go.sum" + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Examples + working-directory: v3 + run: task test:examples + + - name: Run tests (mac) + if: matrix.os == 'macos-latest' + env: + CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 + working-directory: v3 + run: go test -v ./... + + - name: Run tests (windows) + if: matrix.os == 'windows-latest' + working-directory: v3 + run: go test -v ./... + + - name: Run tests (ubuntu) + if: matrix.os == 'ubuntu-latest' + working-directory: v3 + run: > + xvfb-run --auto-servernum + sh -c ' + dbus-update-activation-environment --systemd --all && + go test -v ./... + ' + + - name: Typecheck binding generator output + working-directory: v3 + run: task generator:test:check + + test_js: + name: Run JS Tests + needs: check_approval + runs-on: ubuntu-latest + if: github.base_ref == 'v3-alpha' + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + working-directory: v2/internal/frontend/runtime + + - name: Run tests + run: npm test + working-directory: v2/internal/frontend/runtime + + test_templates: + name: Test Templates + needs: test_go + runs-on: ${{ matrix.os }} + if: github.base_ref == 'v3-alpha' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + template: + - svelte + - svelte-ts + - vue + - vue-ts + - react + - react-ts + - preact + - preact-ts + - lit + - lit-ts + - vanilla + - vanilla-ts + go-version: [1.24] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install linux dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-latest' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + version: 1.0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: "v3/go.sum" + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Wails3 CLI + working-directory: v3 + run: | + task install + wails3 doctor + + - name: Generate template '${{ matrix.template }}' + run: | + mkdir -p ./test-${{ matrix.template }} + cd ./test-${{ matrix.template }} + wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }} + cd ${{ matrix.template }} + wails3 build + + build_results: + if: ${{ always() }} + runs-on: ubuntu-latest + name: v3 Build Results + needs: [test_go, test_js, test_templates] + steps: + - run: | + go_result="${{ needs.test_go.result }}" + js_result="${{ needs.test_js.result }}" + templates_result="${{ needs.test_templates.result }}" + + if [[ $go_result == "success" || $go_result == "skipped" ]] && \ + [[ $js_result == "success" || $js_result == "skipped" ]] && \ + [[ $templates_result == "success" || $templates_result == "skipped" ]]; then + echo "All required jobs succeeded or were skipped" + exit 0 + else + echo "One or more required jobs failed" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 000000000..8fe647c6f --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,160 @@ +name: Build + Test v2 + +on: + push: + branches: [release/*, master, bugfix/*] + workflow_dispatch: + +jobs: + test_go: + name: Run Go Tests + if: github.repository == 'wailsapp/wails' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04, ubuntu-24.04, windows-latest, macos-latest] + go-version: ['1.22'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-22.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + version: 1.0 + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-24.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config libegl1 + version: 1.0 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: ./v2/go.sum + + - name: Run tests (mac) + if: matrix.os == 'macos-latest' + env: + CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (!mac) + if: matrix.os != 'macos-latest' && matrix.os != 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (Ubuntu 24.04) + if: matrix.os == 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v -tags webkit2_41 ./... + + test_js: + name: Run JS Tests + if: github.repository == 'wailsapp/wails' + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + working-directory: v2/internal/frontend/runtime + + - name: Run tests + run: npm test + working-directory: v2/internal/frontend/runtime + + test_templates: + name: Test Templates + needs: test_go + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04] + template: + [ + svelte, + svelte-ts, + vue, + vue-ts, + react, + react-ts, + preact, + preact-ts, + lit, + lit-ts, + vanilla, + vanilla-ts, + plain, + ] + go-version: ['1.22'] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: ./v2/go.sum + + - name: Build Wails CLI + run: | + cd ./v2/cmd/wails + go install + wails -help + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-22.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + version: 1.0 + +# - name: Install linux dependencies ( 22.04 ) +# if: matrix.os == 'ubuntu-22.04' +# run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-24.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config libegl1 + version: 1.0 + +# - name: Install linux dependencies ( 24.04 ) +# if: matrix.os == 'ubuntu-24.04' +# run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + + - name: Generate & Build template '${{ matrix.template }}' + if: matrix.os != 'ubuntu-24.04' + run: | + mkdir -p ./test-${{ matrix.template }} + cd ./test-${{ matrix.template }} + wails init -n ${{ matrix.template }} -t ${{ matrix.template }} -ci + cd ${{ matrix.template }} + wails build -v 2 + + - name: Generate & Build template '${{ matrix.template }}' (ubuntu-24.04) + if: matrix.os == 'ubuntu-24.04' + run: | + mkdir -p ./test-${{ matrix.template }} + cd ./test-${{ matrix.template }} + wails init -n ${{ matrix.template }} -t ${{ matrix.template }} -ci + cd ${{ matrix.template }} + wails build -v 2 -tags webkit2_41 + diff --git a/.github/workflows/build-cross-image.yml b/.github/workflows/build-cross-image.yml new file mode 100644 index 000000000..83b40f2be --- /dev/null +++ b/.github/workflows/build-cross-image.yml @@ -0,0 +1,423 @@ +name: Build Cross-Compiler Image + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch containing Dockerfile' + required: true + default: 'v3-alpha' + sdk_version: + description: 'macOS SDK version' + required: true + default: '14.5' + zig_version: + description: 'Zig version' + required: true + default: '0.14.0' + image_version: + description: 'Image version tag' + required: true + default: 'latest' + skip_tests: + description: 'Skip cross-compilation tests' + required: false + default: 'false' + type: boolean + push: + branches: + - v3-alpha + paths: + - 'v3/internal/commands/build_assets/docker/Dockerfile.cross' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: wailsapp/wails-cross + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image_tag: ${{ steps.vars.outputs.image_version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set build variables + id: vars + run: | + echo "sdk_version=${{ inputs.sdk_version || '14.5' }}" >> $GITHUB_OUTPUT + echo "zig_version=${{ inputs.zig_version || '0.14.0' }}" >> $GITHUB_OUTPUT + echo "image_version=${{ inputs.image_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.vars.outputs.image_version }} + type=raw,value=sdk-${{ steps.vars.outputs.sdk_version }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: v3/internal/commands/build_assets/docker + file: v3/internal/commands/build_assets/docker/Dockerfile.cross + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: | + ${{ steps.meta.outputs.labels }} + io.wails.zig.version=${{ steps.vars.outputs.zig_version }} + io.wails.sdk.version=${{ steps.vars.outputs.sdk_version }} + build-args: | + ZIG_VERSION=${{ steps.vars.outputs.zig_version }} + MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Test cross-compilation for all platforms + test-cross-compile: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Darwin targets (Zig + macOS SDK) - no platform emulation needed + - os: darwin + arch: arm64 + platform: "" + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + platform: "" + expected_file: "Mach-O 64-bit.*x86_64" + # Linux targets (GCC) - need platform to match architecture + - os: linux + arch: amd64 + platform: "linux/amd64" + expected_file: "ELF 64-bit LSB.*x86-64" + - os: linux + arch: arm64 + platform: "linux/arm64" + expected_file: "ELF 64-bit LSB.*ARM aarch64" + # Windows targets (Zig + mingw) - no platform emulation needed + - os: windows + arch: amd64 + platform: "" + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + platform: "" + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + if: matrix.platform != '' + uses: docker/setup-qemu-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a minimal CGO test program + cat > main.go << 'EOF' + package main + + /* + #include + + int add(int a, int b) { + return a + b; + } + */ + import "C" + import "fmt" + + func main() { + result := C.add(1, 2) + fmt.Printf("CGO test: 1 + 2 = %d\n", result) + } + EOF + + cat > go.mod << 'EOF' + module test-cgo + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO) + run: | + cd test-project + PLATFORM_FLAG="" + if [ -n "${{ matrix.platform }}" ]; then + PLATFORM_FLAG="--platform ${{ matrix.platform }}" + fi + + docker run --rm $PLATFORM_FLAG \ + -v "$(pwd):/app" \ + -e APP_NAME="test-cgo" \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + ${{ matrix.os }} ${{ matrix.arch }} + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1) + else + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + + echo "## Library Dependencies for $BINARY" + echo "" + + # Use readelf to show dynamic dependencies + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify expected libraries are linked + echo "" + echo "### Verifying required libraries..." + NEEDED=$(readelf -d "$BINARY" | grep NEEDED) + + MISSING="" + for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do + if echo "$NEEDED" | grep -q "$lib"; then + echo "✅ $lib" + else + echo "❌ $lib MISSING" + MISSING="$MISSING $lib" + fi + done + + if [ -n "$MISSING" ]; then + echo "" + echo "ERROR: Missing required libraries:$MISSING" + exit 1 + fi + + # Test non-CGO builds (pure Go cross-compilation) + test-non-cgo: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - os: darwin + arch: arm64 + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + expected_file: "Mach-O 64-bit.*x86_64" + - os: linux + arch: amd64 + expected_file: "ELF 64-bit LSB" + - os: linux + arch: arm64 + expected_file: "ELF 64-bit LSB.*ARM aarch64" + - os: windows + arch: amd64 + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test non-CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a pure Go test program (no CGO) + cat > main.go << 'EOF' + package main + + import "fmt" + + func main() { + fmt.Println("Pure Go cross-compilation test") + } + EOF + + cat > go.mod << 'EOF' + module test-pure-go + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO) + run: | + cd test-project + + # For non-CGO, we can use any platform since Go handles cross-compilation + # We set CGO_ENABLED=0 to ensure pure Go build + docker run --rm \ + -v "$(pwd):/app" \ + -e APP_NAME="test-pure-go" \ + -e CGO_ENABLED=0 \ + --entrypoint /bin/sh \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + -c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ." + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe" + else + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + + echo "## Library Dependencies for $BINARY (non-CGO)" + echo "" + + # Non-CGO builds should have minimal dependencies (just libc or statically linked) + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify NO GTK/WebKit libraries (since CGO is disabled) + NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true) + if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then + echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!" + exit 1 + else + echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)" + fi + + # Summary job + test-summary: + needs: [build, test-cross-compile, test-non-cgo] + if: always() && inputs.skip_tests != 'true' + runs-on: ubuntu-latest + steps: + - name: Check test results + run: | + echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then + echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then + echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + + # Fail if any test failed + if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then + echo "" + echo "❌ Some tests failed. Check the individual job logs for details." + exit 1 + fi diff --git a/.github/workflows/changelog-v3.yml b/.github/workflows/changelog-v3.yml new file mode 100644 index 000000000..688959b9e --- /dev/null +++ b/.github/workflows/changelog-v3.yml @@ -0,0 +1,216 @@ +name: Changelog Validation (v3) + +on: + pull_request: + branches: [ v3-alpha ] + paths: + - 'docs/src/content/docs/changelog.mdx' + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to validate' + required: true + type: string + +jobs: + validate: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + actions: write + + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.inputs.pr_number) }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN || github.token }} + + - name: Get REAL validation script from v3-alpha + run: | + echo "Fetching the REAL validation script from v3-alpha branch..." + git fetch origin v3-alpha + git checkout origin/v3-alpha -- v3/scripts/validate-changelog.go + + echo "Validation script fetched successfully:" + ls -la v3/scripts/ + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Get PR information + id: pr_info + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + echo "base_ref=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT + else + echo "pr_number=${{ github.event.inputs.pr_number }}" >> $GITHUB_OUTPUT + echo "base_ref=v3-alpha" >> $GITHUB_OUTPUT + fi + + - name: Check changelog modifications + id: changelog_check + run: | + echo "Checking PR #${{ steps.pr_info.outputs.pr_number }} for changelog changes" + git fetch origin ${{ steps.pr_info.outputs.base_ref }} + + if git diff --name-only origin/${{ steps.pr_info.outputs.base_ref }}..HEAD | grep -q "docs/src/content/docs/changelog.mdx"; then + echo "changelog_modified=true" >> $GITHUB_OUTPUT + echo "✅ Changelog was modified in this PR" + else + echo "changelog_modified=false" >> $GITHUB_OUTPUT + echo "ℹ️ Changelog was not modified - skipping validation" + fi + + - name: Get changelog diff + id: get_diff + if: steps.changelog_check.outputs.changelog_modified == 'true' + run: | + echo "Getting diff for changelog changes..." + git diff origin/${{ steps.pr_info.outputs.base_ref }}..HEAD docs/src/content/docs/changelog.mdx | grep "^+" | grep -v "^+++" | sed 's/^+//' > /tmp/pr_added_lines.txt + + echo "Lines added in this PR:" + cat /tmp/pr_added_lines.txt + echo "Total lines added: $(wc -l < /tmp/pr_added_lines.txt)" + + - name: Validate changelog + id: validate + if: steps.changelog_check.outputs.changelog_modified == 'true' + run: | + echo "Running changelog validation..." + cd v3/scripts + OUTPUT=$(go run validate-changelog.go ../../docs/src/content/docs/changelog.mdx /tmp/pr_added_lines.txt 2>&1) + echo "$OUTPUT" + + RESULT=$(echo "$OUTPUT" | grep "VALIDATION_RESULT=" | cut -d'=' -f2) + echo "result=$RESULT" >> $GITHUB_OUTPUT + + - name: Commit fixes + id: commit_fixes + if: steps.validate.outputs.result == 'fixed' + run: | + echo "Committing automatic fixes..." + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Check only the changelog file for changes + if git diff --quiet docs/src/content/docs/changelog.mdx; then + echo "No changes to commit" + echo "committed=false" >> $GITHUB_OUTPUT + else + # Ensure validation script doesn't get committed + echo "v3/scripts/validate-changelog.go" >> .git/info/exclude + # Get the correct branch name to push to + REPO_OWNER="wailsapp" # Always wailsapp for this repo + + if [ "${{ github.event_name }}" = "pull_request" ]; then + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + else + # For manual workflow dispatch, get PR info + PR_INFO=$(gh pr view ${{ steps.pr_info.outputs.pr_number }} --json headRefName,headRepository) + BRANCH_NAME=$(echo "$PR_INFO" | jq -r '.headRefName') + HEAD_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name') + + echo "🔍 PR source branch: $BRANCH_NAME" + echo "🔍 Head repository: $HEAD_REPO" + + # Don't push if this is from a fork or if branch is v3-alpha (main branch) + if [ "$HEAD_REPO" != "wails" ] || [ "$BRANCH_NAME" = "v3-alpha" ]; then + echo "⚠️ Cannot push - either fork or direct v3-alpha branch. Manual fix required." + echo "committed=false" >> $GITHUB_OUTPUT + exit 0 + fi + fi + + echo "Pushing to branch: $BRANCH_NAME in repo: $REPO_OWNER" + + # Only commit the changelog changes, not the validation script + git add docs/src/content/docs/changelog.mdx + git commit -m "🤖 Fix changelog: move entries to Unreleased section" + + # Only push if running on the main wailsapp repository + if [ "${{ github.repository }}" = "wailsapp/wails" ]; then + # Pull latest changes and rebase our commit + git fetch origin $BRANCH_NAME + git rebase origin/$BRANCH_NAME + git push origin HEAD:$BRANCH_NAME + else + echo "⚠️ Running on fork (${{ github.repository }}). Skipping push - manual fix required." + echo "committed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "committed=true" >> $GITHUB_OUTPUT + echo "✅ Changes committed and pushed" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get PR author for tagging + id: pr_author + if: steps.validate.outputs.result && github.event.inputs.pr_number + run: | + PR_AUTHOR=$(gh pr view ${{ steps.pr_info.outputs.pr_number }} --json author --jq '.author.login') + echo "author=$PR_AUTHOR" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment on PR + if: steps.validate.outputs.result && github.event.inputs.pr_number + uses: actions/github-script@v7 + with: + script: | + const result = '${{ steps.validate.outputs.result }}'; + const committed = '${{ steps.commit_fixes.outputs.committed }}'; + const author = '${{ steps.pr_author.outputs.author }}'; + + let message; + if (result === 'success') { + message = '## ✅ Changelog Validation Passed\n\nNo misplaced changelog entries detected.'; + } else if (result === 'fixed' && committed === 'true') { + message = '## 🔧 Changelog Updated\n\nMisplaced entries were automatically moved to the `[Unreleased]` section. The changes have been committed to this PR.'; + } else if (result === 'fixed' || result === 'cannot_fix' || result === 'error') { + // Read the fixed changelog content + const fs = require('fs'); + let fixedContent = ''; + try { + fixedContent = fs.readFileSync('docs/src/content/docs/changelog.mdx', 'utf8'); + } catch (error) { + fixedContent = 'Error reading fixed changelog content'; + } + + message = '## ⚠️ Changelog Validation Issue\\n\\n' + + '@' + author + ' Your PR contains changelog entries that were added to already-released versions. These need to be moved to the `[Unreleased]` section.\\n\\n' + + (committed === 'true' ? + '✅ **Auto-fix applied**: The changes have been automatically committed to this PR.' : + '❌ **Manual fix required**: Please apply the changes shown below manually.') + '\\n\\n' + + '
\\n' + + '📝 Click to see the corrected changelog content\\n\\n' + + '```mdx\\n' + + fixedContent + + '\\n```\\n\\n' + + '
\\n\\n' + + '**What happened?** \\n' + + 'The validation script detected that you added changelog entries to a version section that has already been released (like `v3.0.0-alpha.10`). All new entries should go in the `[Unreleased]` section under the appropriate category (`### Added`, `### Fixed`, etc.).\\n\\n' + + (committed !== 'true' ? '**Action needed:** Please copy the corrected content from above and replace your changelog file.' : ''); + } + + if (message) { + await github.rest.issues.createComment({ + issue_number: ${{ steps.pr_info.outputs.pr_number }}, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); + } + + - name: Fail if validation failed + if: steps.validate.outputs.result == 'cannot_fix' || steps.validate.outputs.result == 'error' + run: | + echo "❌ Changelog validation failed" + exit 1 \ No newline at end of file diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 000000000..b5e8cfd4d --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..d300267f1 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/generate-sponsor-image.yml b/.github/workflows/generate-sponsor-image.yml new file mode 100644 index 000000000..56548ab43 --- /dev/null +++ b/.github/workflows/generate-sponsor-image.yml @@ -0,0 +1,40 @@ +name: Generate Sponsor Image + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + update-sponsors: + name: Update Sponsors + runs-on: ubuntu-latest + if: github.repository == 'wailsapp/wails' + steps: + - uses: actions/checkout@v3 + + - name: Set Node + uses: actions/setup-node@v2 + with: + node-version: 20.x + + - name: Update Sponsors + run: cd scripts/sponsors && chmod 755 ./generate-sponsor-image.sh && ./generate-sponsor-image.sh + env: + SPONSORKIT_GITHUB_TOKEN: ${{ secrets.SPONSORS_TOKEN }} + SPONSORKIT_GITHUB_LOGIN: wailsapp + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: "chore: update sponsors.svg" + add-paths: "website/static/img/sponsors.svg" + title: "chore: update sponsors.svg" + body: | + Auto-generated by the sponsor image workflow + + [skip ci] [skip actions] + branch: update-sponsors + base: master + delete-branch: true + draft: false diff --git a/.github/workflows/issue-triage-automation.yml b/.github/workflows/issue-triage-automation.yml new file mode 100644 index 000000000..99159a2f5 --- /dev/null +++ b/.github/workflows/issue-triage-automation.yml @@ -0,0 +1,77 @@ +name: Issue Triage Automation + +on: + issues: + types: [opened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + steps: + # Request more info for unclear bug reports + - name: Request more info + uses: actions/github-script@v6 + if: | + contains(github.event.issue.labels.*.name, 'bug') && + !contains(github.event.issue.body, 'wails doctor') && + !contains(github.event.issue.body, 'reproduction') + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `👋 Thanks for reporting this issue! To help us investigate, could you please: + + 1. Add the output of \`wails doctor\` if not already included + 2. Provide clear steps to reproduce the issue + 3. If possible, create a minimal reproduction of the issue + + This will help us resolve your issue much faster. Thank you!` + }); + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['awaiting feedback'] + }); + + # Prioritize security issues + - name: Prioritize security issues + uses: actions/github-script@v6 + if: contains(github.event.issue.labels.*.name, 'security') + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['high-priority'] + }); + + # Tag version-specific issues for project boards + - name: Add to v2 project + uses: actions/github-script@v6 + if: | + contains(github.event.issue.labels.*.name, 'v2-only') && + !contains(github.event.issue.labels.*.name, 'v3-alpha') + with: + script: | + // Replace PROJECT_ID with your actual GitHub project ID + // This is a placeholder as the actual implementation would require + // GraphQL API calls to add to a project board + console.log('Would add to v2 project board'); + + # Tag version-specific issues for project boards + - name: Add to v3 project + uses: actions/github-script@v6 + if: contains(github.event.issue.labels.*.name, 'v3-alpha') + with: + script: | + // Replace PROJECT_ID with your actual GitHub project ID + // This is a placeholder as the actual implementation would require + // GraphQL API calls to add to a project board + console.log('Would add to v3 project board'); diff --git a/.github/workflows/nightly-release-v3.yml b/.github/workflows/nightly-release-v3.yml new file mode 100644 index 000000000..ae56ba7bc --- /dev/null +++ b/.github/workflows/nightly-release-v3.yml @@ -0,0 +1,210 @@ +name: Nightly Release v3-alpha + +on: + schedule: + - cron: '0 2 * * *' # 2 AM UTC daily + workflow_dispatch: + inputs: + force_release: + description: 'Force release even if no changes detected' + required: false + default: false + type: boolean + dry_run: + description: 'Run in dry-run mode (no actual release)' + required: false + default: true + type: boolean + +jobs: + nightly-release: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: read + actions: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.24' + cache: true + cache-dependency-path: 'v3/go.sum' + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Configure git to use the token for authentication + git config --global url."https://x-access-token:${{ secrets.WAILS_REPO_TOKEN || github.token }}@github.com/".insteadOf "https://github.com/" + + - name: Check for existing release tag + id: check_tag + run: | + if git describe --tags --exact-match HEAD 2>/dev/null; then + echo "has_tag=true" >> $GITHUB_OUTPUT + echo "tag=$(git describe --tags --exact-match HEAD)" >> $GITHUB_OUTPUT + else + echo "has_tag=false" >> $GITHUB_OUTPUT + echo "tag=" >> $GITHUB_OUTPUT + fi + + - name: Check for unreleased changelog content + id: changelog_check + run: | + echo "🔍 Checking UNRELEASED_CHANGELOG.md for content..." + + # Run the release script in check mode to see if there's content + cd v3/tasks/release + + # Use the release script itself to check for content + if go run release.go --check-only 2>/dev/null; then + echo "has_unreleased_content=true" >> $GITHUB_OUTPUT + echo "✅ Found unreleased changelog content" + else + echo "has_unreleased_content=false" >> $GITHUB_OUTPUT + echo "ℹ️ No unreleased changelog content found" + fi + + - name: Quick change detection and early exit + id: quick_check + run: | + echo "🔍 Quick check for changes to determine if we should continue..." + + # First check if we have unreleased changelog content + if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then + echo "✅ Found unreleased changelog content, proceeding with release" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + echo "reason=Found unreleased changelog content" >> $GITHUB_OUTPUT + exit 0 + fi + + # If no unreleased changelog content, check for git changes as fallback + echo "No unreleased changelog content found, checking for git changes..." + + # Check if current commit has a release tag + if git describe --tags --exact-match HEAD 2>/dev/null; then + CURRENT_TAG=$(git describe --tags --exact-match HEAD) + echo "Current commit has release tag: $CURRENT_TAG" + + # For tagged commits, check if there are changes since the tag + COMMIT_COUNT=$(git rev-list ${CURRENT_TAG}..HEAD --count) + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "reason=No changes since existing tag $CURRENT_TAG and no unreleased changelog content" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + fi + else + # No current tag, check against latest release + LATEST_TAG=$(git tag --list "v3.0.0-alpha.*" | sort -V | tail -1) + if [ -z "$LATEST_TAG" ]; then + echo "No previous release found, proceeding with release" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + else + COMMIT_COUNT=$(git rev-list ${LATEST_TAG}..HEAD --count) + if [ "$COMMIT_COUNT" -gt 0 ]; then + echo "Found $COMMIT_COUNT commits since $LATEST_TAG" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "reason=No changes since latest release $LATEST_TAG and no unreleased changelog content" >> $GITHUB_OUTPUT + fi + fi + fi + + - name: Early exit - No changes detected + if: | + steps.quick_check.outputs.should_continue == 'false' && + github.event.inputs.force_release != 'true' + run: | + echo "🛑 EARLY EXIT: ${{ steps.quick_check.outputs.reason }}" + echo "" + echo "ℹ️ No changes detected since last release and force_release is not enabled." + echo " Workflow will exit early to save resources." + echo "" + echo " To force a release anyway, run this workflow with 'force_release=true'" + echo "" + echo "## 🛑 Early Exit Summary" >> $GITHUB_STEP_SUMMARY + echo "**Reason:** ${{ steps.quick_check.outputs.reason }}" >> $GITHUB_STEP_SUMMARY + echo "**Action:** Workflow exited early to save resources" >> $GITHUB_STEP_SUMMARY + echo "**Force Release:** Set 'force_release=true' to override this behavior" >> $GITHUB_STEP_SUMMARY + exit 0 + + - name: Continue with release process + if: | + steps.quick_check.outputs.should_continue == 'true' || + github.event.inputs.force_release == 'true' + run: | + echo "✅ Proceeding with release process..." + if [ "${{ github.event.inputs.force_release }}" == "true" ]; then + echo "🔨 FORCE RELEASE: Overriding change detection" + fi + + - name: Run release script + id: release + if: | + steps.quick_check.outputs.should_continue == 'true' || + github.event.inputs.force_release == 'true' + env: + WAILS_REPO_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + run: | + cd v3/tasks/release + ARGS=() + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + ARGS+=(--dry-run) + fi + go run release.go "${ARGS[@]}" + + - name: Summary + if: always() + run: | + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + echo "## 🧪 DRY RUN Release Summary" >> $GITHUB_STEP_SUMMARY + else + echo "## 🚀 Nightly Release Summary" >> $GITHUB_STEP_SUMMARY + fi + echo "================================" >> $GITHUB_STEP_SUMMARY + + if [ -n "${{ steps.release.outputs.release_version }}" ]; then + echo "- **Version:** ${{ steps.release.outputs.release_version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** ${{ steps.release.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** ${{ steps.release.outcome == 'success' && '✅ Success' || '⚠️ Failed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Mode:** ${{ steps.release.outputs.release_dry_run == 'true' && '🧪 Dry Run' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.release.outputs.release_url }}" ]; then + echo "- **Release URL:** ${{ steps.release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Changelog" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then + echo "✅ Unreleased changelog processed and reset." >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No unreleased changelog content detected." >> $GITHUB_STEP_SUMMARY + fi + else + echo "- Release script did not run (skipped or failed before execution)." >> $GITHUB_STEP_SUMMARY + fi + diff --git a/.github/workflows/pr-master.yml b/.github/workflows/pr-master.yml new file mode 100644 index 000000000..c961b4434 --- /dev/null +++ b/.github/workflows/pr-master.yml @@ -0,0 +1,104 @@ +# Updated to ensure "Run Go Tests" runs for pull requests as expected. +# Key fix: the test_go job previously required github.event.review.state == 'approved' +# which only exists on pull_request_review events. That prevented the job from +# running for regular pull_request events (opened / synchronize / reopened). +# New logic: run tests for pull_request events, and also allow running when a +# pull_request_review is submitted with state == 'approved'. +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - master + pull_request_review: + types: [submitted] + branches: + - master + workflow_dispatch: {} + +name: PR Checks (master) + +jobs: + check_docs: + name: Check Docs + if: ${{ github.repository == 'wailsapp/wails' && github.base_ref == 'master' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Verify Changed files + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 + id: verify-changed-files + with: + files: | + website/**/*.mdx + website/**/*.md + - name: Run step only when files change. + if: steps.verify-changed-files.outputs.files_changed != 'true' + run: | + echo "::warning::Feature branch does not contain any changes to the website." + + test_go: + name: Run Go Tests + runs-on: ${{ matrix.os }} + # Run when: + # - the event is a pull_request (opened/synchronize/reopened) OR + # - the event is a pull_request_review AND the review state is 'approved' + # plus other existing filters (not the update-sponsors branch, repo and base_ref) + if: > + github.repository == 'wailsapp/wails' && + github.base_ref == 'master' && + github.event.pull_request.head.ref != 'update-sponsors' && + ( + github.event_name == 'pull_request' || + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') + ) + strategy: + matrix: + os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04] + go-version: ['1.23'] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install linux dependencies (22.04) + if: matrix.os == 'ubuntu-22.04' + run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + + - name: Install linux dependencies (24.04) + if: matrix.os == 'ubuntu-24.04' + run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Run tests (mac) + if: matrix.os == 'macos-latest' + env: + CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (!mac) + if: matrix.os != 'macos-latest' && matrix.os != 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (Ubuntu 24.04) + if: matrix.os == 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v -tags webkit2_41 ./... + + # This job will run instead of test_go for the update-sponsors branch + skip_tests: + name: Skip Tests (Sponsor Update) + if: github.event.pull_request.head.ref == 'update-sponsors' + runs-on: ubuntu-latest + steps: + - name: Skip tests for sponsor updates + run: | + echo "Skipping tests for sponsor update branch" + echo "This is an automated update of the sponsors image." + continue-on-error: true diff --git a/.github/workflows/projects.yml b/.github/workflows/projects.yml new file mode 100644 index 000000000..3b81c64e7 --- /dev/null +++ b/.github/workflows/projects.yml @@ -0,0 +1,17 @@ +name: Update Project + +on: + project_card: + types: [ moved ] + +jobs: + projectcardautolabel_job: + runs-on: ubuntu-latest + if: github.repository == 'wailsapp/wails' + steps: + - name: Run ProjectCard AutoLabel + id: runprojectcardautolabel + uses: Matticusau/projectcard-autolabel@v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + autolabel-config: '[{"column": "TODO", "add_labels":["TODO"], "remove_labels":["In Progress", "Ready For Testing"]},{"column":"In progress", "add_labels":["In Progress"], "remove_labels":["TODO", "Ready For Testing"]},{"column":"In review", "add_labels":["Ready For Testing"], "remove_labels":["TODO", "In Progress"]}, {"column":"Done", "add_labels":["Done"], "remove_labels":["TODO", "In Progress", "Ready For Testing"]}]' \ No newline at end of file diff --git a/.github/workflows/runtime.yml b/.github/workflows/runtime.yml new file mode 100644 index 000000000..2c97b628b --- /dev/null +++ b/.github/workflows/runtime.yml @@ -0,0 +1,29 @@ +name: Runtime +on: + push: + branches: + - v2-alpha + paths: + - 'v2/internal/frontend/runtime/**' +jobs: + rebuild-runtime: + name: Rebuild the runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v2 + with: + node-version: 14.17.6 + cache: 'npm' + cache-dependency-path: v2/internal/frontend/runtime/package-lock.json + - run: npm install + working-directory: v2/internal/frontend/runtime + - run: npm run build + working-directory: v2/internal/frontend/runtime + + - name: Commit changes + uses: devops-infra/action-commit-push@master + with: + github_token: "${{ secrets.GITHUB_TOKEN }}" + commit_prefix: "[AUTO]" + commit_message: "The runtime was rebuilt" diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..a59818660 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,25 @@ +on: + workflow_dispatch: {} + pull_request: {} + push: + branches: + - main + - master + - v3-alpha + paths: + - .github/workflows/semgrep.yml + schedule: + # random HH:MM to avoid a load spike on GitHub Actions at 00:00 + - cron: 14 16 * * * +name: Semgrep +jobs: + semgrep: + name: semgrep/ci + runs-on: ubuntu-24.04 + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: returntocorp/semgrep + steps: + - uses: actions/checkout@v3 + - run: semgrep ci diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..c4ffd25fe --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,57 @@ +name: Mark and Close Stale Issues + +on: + schedule: + - cron: '0 1 * * *' # Run at 1 AM UTC every day + workflow_dispatch: # Allow manual triggering + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + # General settings + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 45 + days-before-close: 10 + stale-issue-label: 'stale' + operations-per-run: 250 # Increased from 50 to 250 + + # Issue specific settings + stale-issue-message: | + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs within the next 10 days. + + If this issue is still relevant, please add a comment to keep it open. + Thank you for your contributions. + + close-issue-message: | + This issue has been automatically closed due to lack of activity. + Please feel free to reopen it if it's still relevant. + + # PR specific settings - We will not mark PRs as stale + days-before-pr-stale: -1 # Disable PR staling + days-before-pr-close: -1 # Disable PR closing + + # Exemptions + exempt-issue-labels: 'pinned,security,onhold,inprogress,Selected For Development,bug,enhancement,v3-alpha,high-priority' + exempt-all-issue-milestones: true + exempt-all-issue-assignees: true + + # Protection for existing issues + exempt-issue-created-before: '2024-01-01T00:00:00Z' + start-date: '2025-06-01T00:00:00Z' # Don't start checking until June 1, 2025 + + # Only process issues, not PRs + only-labels: '' + any-of-labels: '' + remove-stale-when-updated: true + + # Debug options + debug-only: false # Set to true to test without actually marking issues + ascending: true # Process older issues first diff --git a/.github/workflows/sync-translated-documents.yml b/.github/workflows/sync-translated-documents.yml new file mode 100644 index 000000000..0aa06f11e --- /dev/null +++ b/.github/workflows/sync-translated-documents.yml @@ -0,0 +1,41 @@ +name: Sync Translations + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + sync-translated-documents: + runs-on: ubuntu-latest + if: github.repository == 'wailsapp/wails' + steps: + - uses: actions/checkout@v3 + + - name: Setup Nodejs + uses: actions/setup-node@v2 + with: + node-version: 20.x + + - name: Install Task + uses: arduino/setup-task@v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Sync Translated Documents + env: + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + working-directory: ./website + run: task crowdin:pull + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + commit-message: "docs: sync translations" + title: "docs: sync translations" + body: "- [x] Sync translated documents" + branch: chore/sync-translations + labels: translation + delete-branch: true + draft: true diff --git a/.github/workflows/test-nightly-releases.yml b/.github/workflows/test-nightly-releases.yml new file mode 100644 index 000000000..63df09935 --- /dev/null +++ b/.github/workflows/test-nightly-releases.yml @@ -0,0 +1,216 @@ +name: Test Nightly Releases (Dry Run) + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actual releases)' + required: false + default: true + type: boolean + test_branch: + description: 'Branch to test against' + required: false + default: 'master' + type: string + +env: + GO_VERSION: '1.24' + +jobs: + test-permissions: + name: Test Release Permissions + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + steps: + - name: Check if user is authorized + id: check + run: | + # Test authorization logic + AUTHORIZED_USERS="leaanthony" + + if [[ "$AUTHORIZED_USERS" == *"${{ github.actor }}"* ]]; then + echo "✅ User ${{ github.actor }} is authorized" + echo "authorized=true" >> $GITHUB_OUTPUT + else + echo "❌ User ${{ github.actor }} is not authorized" + echo "authorized=false" >> $GITHUB_OUTPUT + fi + + test-changelog-extraction: + name: Test Changelog Extraction + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.test_branch }} + fetch-depth: 0 + + - name: Test v2 changelog extraction + run: | + echo "🧪 Testing v2 changelog extraction..." + CHANGELOG_FILE="website/src/pages/changelog.mdx" + + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "❌ v2 changelog file not found" + exit 1 + fi + + # Extract unreleased section + awk ' + /^## \[Unreleased\]/ { found=1; next } + found && /^## / { exit } + found && !/^$/ { print } + ' $CHANGELOG_FILE > v2_release_notes.md + + echo "📝 v2 changelog content (first 10 lines):" + head -10 v2_release_notes.md || echo "No content found" + echo "Total lines: $(wc -l < v2_release_notes.md)" + + - name: Test v3 changelog extraction (if accessible) + run: | + echo "🧪 Testing v3 changelog extraction..." + + if git show v3-alpha:docs/src/content/docs/changelog.mdx > /dev/null 2>&1; then + echo "✅ v3 changelog accessible" + + git show v3-alpha:docs/src/content/docs/changelog.mdx | awk ' + /^## \[Unreleased\]/ { found=1; next } + found && /^## / { exit } + found && !/^$/ { print } + ' > v3_release_notes.md + + echo "📝 v3 changelog content (first 10 lines):" + head -10 v3_release_notes.md || echo "No content found" + echo "Total lines: $(wc -l < v3_release_notes.md)" + else + echo "⚠️ v3 changelog not accessible from current context" + fi + + test-version-detection: + name: Test Version Detection + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + outputs: + v2_current_version: ${{ steps.versions.outputs.v2_current }} + v2_next_patch: ${{ steps.versions.outputs.v2_next_patch }} + v2_next_minor: ${{ steps.versions.outputs.v2_next_minor }} + v2_next_major: ${{ steps.versions.outputs.v2_next_major }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Test version detection logic + id: versions + run: | + echo "🧪 Testing version detection..." + + # Test v2 version parsing + if [ -f "v2/cmd/wails/internal/version.txt" ]; then + CURRENT_V2=$(cat v2/cmd/wails/internal/version.txt | sed 's/^v//') + echo "Current v2 version: v$CURRENT_V2" + echo "v2_current=v$CURRENT_V2" >> $GITHUB_OUTPUT + + # Parse and increment + IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_V2" + MAJOR=${VERSION_PARTS[0]} + MINOR=${VERSION_PARTS[1]} + PATCH=${VERSION_PARTS[2]} + + PATCH_VERSION="v$MAJOR.$MINOR.$((PATCH + 1))" + MINOR_VERSION="v$MAJOR.$((MINOR + 1)).0" + MAJOR_VERSION="v$((MAJOR + 1)).0.0" + + echo "v2_next_patch=$PATCH_VERSION" >> $GITHUB_OUTPUT + echo "v2_next_minor=$MINOR_VERSION" >> $GITHUB_OUTPUT + echo "v2_next_major=$MAJOR_VERSION" >> $GITHUB_OUTPUT + + echo "✅ Patch: v$CURRENT_V2 → $PATCH_VERSION" + echo "✅ Minor: v$CURRENT_V2 → $MINOR_VERSION" + echo "✅ Major: v$CURRENT_V2 → $MAJOR_VERSION" + else + echo "❌ v2 version file not found" + fi + + test-commit-analysis: + name: Test Commit Analysis + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Test commit analysis + run: | + echo "🧪 Testing commit analysis..." + + # Get recent commits for testing + echo "Recent commits:" + git log --oneline -10 + + # Test conventional commit detection + RECENT_COMMITS=$(git log --oneline --since="7 days ago") + echo "Commits from last 7 days:" + echo "$RECENT_COMMITS" + + # Analyze for release type + RELEASE_TYPE="patch" + if echo "$RECENT_COMMITS" | grep -q "feat!\|fix!\|BREAKING CHANGE:"; then + RELEASE_TYPE="major" + elif echo "$RECENT_COMMITS" | grep -q "feat\|BREAKING CHANGE"; then + RELEASE_TYPE="minor" + fi + + echo "✅ Detected release type: $RELEASE_TYPE" + + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [test-permissions, test-changelog-extraction, test-version-detection, test-commit-analysis] + if: always() + steps: + - name: Print test results + run: | + echo "# 🧪 Nightly Release Workflow Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-permissions.result }}" == "success" ]; then + echo "✅ **Permissions Test**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Permissions Test**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-changelog-extraction.result }}" == "success" ]; then + echo "✅ **Changelog Extraction**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Changelog Extraction**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-version-detection.result }}" == "success" ]; then + echo "✅ **Version Detection**: Passed" >> $GITHUB_STEP_SUMMARY + echo " - Current v2: ${{ needs.test-version-detection.outputs.v2_current_version }}" >> $GITHUB_STEP_SUMMARY + echo " - Next patch: ${{ needs.test-version-detection.outputs.v2_next_patch }}" >> $GITHUB_STEP_SUMMARY + echo " - Next minor: ${{ needs.test-version-detection.outputs.v2_next_minor }}" >> $GITHUB_STEP_SUMMARY + echo " - Next major: ${{ needs.test-version-detection.outputs.v2_next_major }}" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Version Detection**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-commit-analysis.result }}" == "success" ]; then + echo "✅ **Commit Analysis**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Commit Analysis**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This was a dry-run test. No actual releases were created." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/unreleased-changelog-trigger.yml b/.github/workflows/unreleased-changelog-trigger.yml new file mode 100644 index 000000000..8cfe85de0 --- /dev/null +++ b/.github/workflows/unreleased-changelog-trigger.yml @@ -0,0 +1,129 @@ +name: Auto Release on Changelog Update + +on: + push: + branches: + - v3-alpha + paths: + - 'v3/UNRELEASED_CHANGELOG.md' + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actual release)' + required: false + default: false + type: boolean + +jobs: + check-permissions: + name: Check Release Permissions + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + steps: + - name: Check if user is authorized for releases + id: check + run: | + # Only allow specific users to trigger releases + AUTHORIZED_USERS="leaanthony" + + if [[ "$AUTHORIZED_USERS" == *"${{ github.actor }}"* ]]; then + echo "✅ User ${{ github.actor }} is authorized for releases" + echo "authorized=true" >> $GITHUB_OUTPUT + else + echo "❌ User ${{ github.actor }} is not authorized for releases" + echo "authorized=false" >> $GITHUB_OUTPUT + fi + + trigger-release: + name: Trigger v3-alpha Release + permissions: + contents: read + actions: write + runs-on: ubuntu-latest + needs: check-permissions + if: needs.check-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + + - name: Check for unreleased changelog content + id: changelog_check + run: | + echo "🔍 Checking UNRELEASED_CHANGELOG.md for content..." + + cd v3 + # Check if UNRELEASED_CHANGELOG.md has actual content beyond the template + if [ -f "UNRELEASED_CHANGELOG.md" ]; then + # Use a simple check for actual content (bullet points starting with -) + CONTENT_LINES=$(grep -E "^\s*-\s+[^[:space:]]" UNRELEASED_CHANGELOG.md | wc -l) + if [ "$CONTENT_LINES" -gt 0 ]; then + echo "✅ Found $CONTENT_LINES content lines in UNRELEASED_CHANGELOG.md" + echo "has_content=true" >> $GITHUB_OUTPUT + else + echo "ℹ️ No actual content found in UNRELEASED_CHANGELOG.md" + echo "has_content=false" >> $GITHUB_OUTPUT + fi + else + echo "❌ UNRELEASED_CHANGELOG.md not found" + echo "has_content=false" >> $GITHUB_OUTPUT + fi + + - name: Trigger nightly release workflow + if: steps.changelog_check.outputs.has_content == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + script: | + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'nightly-release-v3.yml', + ref: 'v3-alpha', + inputs: { + force_release: 'true', + dry_run: '${{ github.event.inputs.dry_run || "false" }}' + } + }); + + console.log('🚀 Successfully triggered nightly release workflow'); + console.log(`Workflow dispatch response status: ${response.status}`); + + // Create a summary + core.summary + .addHeading('🚀 Auto Release Triggered') + .addRaw('The v3-alpha release workflow has been automatically triggered due to changes in UNRELEASED_CHANGELOG.md') + .addTable([ + [{data: 'Trigger', header: true}, {data: 'Value', header: true}], + ['Repository', context.repo.repo], + ['Branch', 'v3-alpha'], + ['Actor', context.actor], + ['Dry Run', '${{ github.event.inputs.dry_run || "false" }}'], + ['Force Release', 'true'] + ]) + .addRaw('\n---\n*This release was automatically triggered by the unreleased-changelog-trigger workflow*') + .write(); + + - name: No content found + if: steps.changelog_check.outputs.has_content == 'false' + run: | + echo "ℹ️ No content found in UNRELEASED_CHANGELOG.md, skipping release trigger" + echo "## ℹ️ No Release Triggered" >> $GITHUB_STEP_SUMMARY + echo "**Reason:** UNRELEASED_CHANGELOG.md does not contain actual changelog content" >> $GITHUB_STEP_SUMMARY + echo "**Action:** No release workflow was triggered" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To trigger a release, add actual changelog entries to the UNRELEASED_CHANGELOG.md file." >> $GITHUB_STEP_SUMMARY + + - name: Unauthorized user + if: needs.check-permissions.outputs.authorized == 'false' + run: | + echo "❌ User ${{ github.actor }} is not authorized to trigger releases" + echo "## ❌ Unauthorized Release Attempt" >> $GITHUB_STEP_SUMMARY + echo "**User:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "**Action:** Release trigger was blocked due to insufficient permissions" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Only authorized users can trigger automatic releases via changelog updates." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/upload-source-documents.yml b/.github/workflows/upload-source-documents.yml new file mode 100644 index 000000000..69d6c3e48 --- /dev/null +++ b/.github/workflows/upload-source-documents.yml @@ -0,0 +1,41 @@ +name: Upload Source Documents + +on: + push: + branches: [master] + workflow_dispatch: + +jobs: + push_files_to_crowdin: + name: Push files to Crowdin + if: github.repository == 'wailsapp/wails' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Verify Changed files + id: changed-files + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 + with: + files: | + website/**/*.mdx + website/**/*.md + website/**/*.json + + - name: Setup Nodejs + uses: actions/setup-node@v2 + with: + node-version: 20.x + + - name: Setup Task + uses: arduino/setup-task@v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload source documents + if: steps.changed-files.outputs.any_changed == 'true' + env: + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + working-directory: ./website + run: task crowdin:push diff --git a/.gitignore b/.gitignore index 3bd91e3e9..e7888b44a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,27 @@ examples/**/example* !examples/**/*.* cmd/wails/wails .DS_Store -tmp \ No newline at end of file +tmp +node_modules/ +package.json.md5 +v2/test/**/frontend/dist +v2/test/**/build/ +v2/test/frameless/icon.png +v2/test/hidden/icon.png +v2/test/kitchensink/frontend/public/bundle.* +v2/pkg/parser/testproject/frontend/wails +v2/test/kitchensink/frontend/public +v2/test/kitchensink/build/darwin/desktop/kitchensink +v2/test/kitchensink/frontend/package.json.md5 +.idea/ +v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ +.env +/website/static/img/.cache.json + +/v3/.task +/v3/examples/build/bin/testapp +/websitev3/site/ +/v3/examples/plugins/bin/testapp + +# Temporary called mkdocs, should be renamed to more standard -website or similar +/mkdocs-website/site diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..52b962c55 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +website +v2 +v3 \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 000000000..685d8b6e7 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,6 @@ +overrides: + - files: + - "**/*.md" + options: + printWidth: 80 + proseWrap: always diff --git a/.replit b/.replit new file mode 100644 index 000000000..619bd7227 --- /dev/null +++ b/.replit @@ -0,0 +1,8 @@ +modules = ["go-1.21", "web", "nodejs-20"] +run = "go run v2/cmd/wails/main.go" + +[nix] +channel = "stable-24_05" + +[deployment] +run = ["sh", "-c", "go run v2/cmd/wails/main.go"] diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index bc0d6013e..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/cmd/wails/main.go", - "env": {}, - "args": [ - "setup" - ] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..62b09b25f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +The current changelog may be found at: https://wails.io/changelog/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..aa53c412a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +The current Contribution Guidelines can be found at: https://wails.io/community-guide diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..a7b5a60ab --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,2 @@ + +The latest contributors list may be found at: https://wails.io/credits#contributors \ No newline at end of file diff --git a/LICENSE b/LICENSE index ebbfa928b..28f2a3683 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 wailsapp +Copyright (c) 2018-Present Lea Anthony Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.de.md b/README.de.md new file mode 100644 index 000000000..5df35de5b --- /dev/null +++ b/README.de.md @@ -0,0 +1,160 @@ +

+
+

+ +

+Erschaffe Desktop Anwendungen mit Go & Web Technologien. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) + + + +
+ +## Inhaltsverzeichnis + +- [Inhaltsverzeichnis](#inhaltsverzeichnis) +- [Einführung](#einführung) +- [Funktionen](#funktionen) + - [Roadmap](#roadmap) +- [Loslegen](#loslegen) +- [Sponsoren](#sponsoren) +- [FAQ](#faq) +- [Sterne Überblick](#sterne-überblick) +- [Mitwirkende](#mitwirkende) +- [Lizenz](#lizenz) +- [Inspiration](#inspiration) + +## Einführung + +Die herkömmliche Methode zur Bereitstellung von Web-Interfaces für Go ist über einen eingebauten Webserver. +Wails nutzt einen anderen Weg. Es kann sowohl Go-Code als auch ein Web-Frontend in eine einzige Datei bauen. +Beigelieferte Werkzeuge übernehmen die Projekterstellung, den Kompilierungsprozess und das bauen. +Du musst nur kreativ werden. + +## Funktionen + +- Nutze Standard Go für das Backend +- Nutze eine Frontend Technologie mit der du dich bereits auskennst um dein UI zu bauen. +- Erschaffe schnell und einfach Frontends mit vorgefertigten Vorlagen für deine Go-Programme +- Nutze Javascript um Go Methoden aufzurufen +- Automatisch generierte Typescript Definitionen für deine Go Strukturen und Methoden +- Native Dialoge und Menüs +- Native Dark-/Lightmode Unterstützung +- Unterstützt moderne Transluzenz- und Milchglaseffekte +- Vereinheitlichtes Eventsystem zwischen Go und Javascript +- Leistungsstarkes CLI-Tool zum einfachen erstellen und bauen von Projekten +- Multiplattformen +- Nutze native Render-Engines - _keine eingebetteten Browser_! + +### Roadmap + +Die Projekt Roadmap kann [hier](https://github.com/wailsapp/wails/discussions/1484) gefunden werden. Bitte lies diese +durch bevor du eine Idee vorschlägst + +## Loslegen + +Die Installationsinstruktionen sind auf der [offiziellen Website](https://wails.io/docs/gettingstarted/installation). + +## Sponsoren + +Dieses Projekt wird von diesen freundlichen Leuten und Firmen unterstützt: + + +

+ +

+ +## FAQ + +- Ist das eine Alternative zu Electron? + + Hängt von deinen Anforderungen ab. Wails wurde entwickelt um das Go-Programmieren leicht zu machen und effiziente + Desktop-Anwendungen zu erstellen oder ein Frontend zu einer bestehenden Anwendung hinzuzufügen. + Wails bietet native Elemente wie Dialoge und Menüs und könnte somit als eine leichte effiziente Electron-Alternative + betrachtet werden. + +- Für wen ist dieses projekt geeignet? + + Go Entwickler, die ein HTML/CSS/JS-Frontend in ihre Anwendung integrieren möchten, ohne einen Webserver zu erstellen und + einen Browser öffnen zu müssen, um dieses zu sehen + +- Wie kam es zu diesem Namen? + + Als ich WebView sah dachte ich "Was ich wirklich will, ist ein Werkzeug für die Erstellung von WebView Anwendungen so wie Rails für Ruby". + Also war es zunächst ein Wortspiel (Webview on Rails). Zufälligerweise ist es auch ein Homophon des englischen Namens des [Landes](https://en.wikipedia.org/wiki/Wales), aus dem ich komme. + Also ist es dabei geblieben. + +## Sterne Überblick + + + + + + Star History Chart + + + +## Mitwirkende + +Die Liste der Mitwirkenden wird zu groß für diese Readme. All die fantastischen Menschen, die zu diesem +Projekt beigetragen haben, haben [hier](https://wails.io/credits#contributors) ihre eigene Seite. + +## Lizenz + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +Dieses Projekt wurde hauptsächlich zu den folgenden Alben entwickelt + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.es.md b/README.es.md new file mode 100644 index 000000000..277d1c1fd --- /dev/null +++ b/README.es.md @@ -0,0 +1,169 @@ +

+
+

+ +

+ Construye aplicaciones de escritorio usando Go y tecnologías web. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Tabla de Contenidos + +- [Tabla de Contenidos](#tabla-de-contenidos) +- [Introducción](#introducción) +- [Funcionalidades](#funcionalidades) + - [Plan de Trabajo](#plan-de-trabajo) +- [Empezando](#empezando) +- [Patrocinadores](#patrocinadores) +- [Preguntas Frecuentes](#preguntas-frecuentes) +- [Estrellas a lo Largo del Tiempo](#estrellas-a-lo-largo-del-tiempo) +- [Colaboradores](#colaboradores) +- [Licencia](#licencia) +- [Inspiración](#inspiración) + +## Introducción + +El método tradicional para proveer una interfaz web en programas hechos con Go +es a través del servidor web incorporado. Wails ofrece un enfoque diferente al +permitir combinar el código hecho en Go con un frontend web en un solo archivo +binario. Las herramientas que proporcionamos facilitan este trabajo para ti, al +crear, compilar y empaquetar tu proyecto. ¡Lo único que debes hacer es ponerte +creativo! + +## Funcionalidades + +- Utiliza Go estándar para el backend +- Utiliza cualquier tecnología frontend con la que ya estés familiarizado para + construir tu interfaz de usuario +- Crea rápidamente interfaces de usuario enriquecidas para tus programas en Go + utilizando plantillas predefinidas +- Invoca fácilmente métodos de Go desde Javascript +- Definiciones de Typescript generadas automáticamente para tus structs y + métodos de Go +- Diálogos y menús nativos +- Soporte nativo de modo oscuro / claro +- Soporte de translucidez y efectos de ventana esmerilada +- Sistema de eventos unificado entre Go y Javascript +- Herramienta CLI potente para generar y construir tus proyectos rápidamente +- Multiplataforma +- Usa motores de renderizado nativos - ¡_sin navegador integrado_! + +### Plan de Trabajo + +El plan de trabajo se puede encontrar +[aqui](https://github.com/wailsapp/wails/discussions/1484). Por favor, +consúltalo antes de abrir una solicitud de mejora. + +## Empezando + +Las instrucciones de instalacion se encuentran en nuestra +[pagina web oficial](https://wails.io/docs/gettingstarted/installation). + +## Patrocinadores + +Este Proyecto cuenta con el apoyo de estas amables personas/ compañías: + + +

+ +

+ +## Preguntas Frecuentes + +- ¿Es esta una alternativa a Electron? + + Depende de tus requisitos. Está diseñado para facilitar a los programadores de + Go la creación de aplicaciones de escritorio livianas o agregar una interfaz + gráfica a sus aplicaciones existentes. Wails ofrece elementos nativos como + menús y diálogos, por lo que podría considerarse una alternativa liviana a + Electron. + +- ¿A quien esta dirigido este proyecto? + + El proyecto esta dirigido a programadores de Go que desean integrar una + interfaz HMTL/JS/CSS en sus aplicaciones, sin tener que recurrir a la creación + de un servidor y abrir el navegador para visualizarla. + +- ¿Cual es el significado del nombre? + + Cuando vi WebView, pensé: "Lo que realmente quiero es una herramienta para + construir una aplicación WebView, algo similar a lo que Rails es para Ruby". + Así que inicialmente fue un juego de palabras (WebView en Rails). Además, por + casualidad, también es homófono del nombre en inglés del + [país](https://en.wikipedia.org/wiki/Wales) del que provengo. Así que se quedó + con ese nombre. + +## Estrellas a lo Largo del Tiempo + +[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## Colaboradores + +¡La lista de colaboradores se está volviendo demasiado grande para el archivo +readme! Todas las personas increíbles que han contribuido a este proyecto tienen +su propia página [aqui](https://wails.io/credits#contributors). + +## Licencia + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiración + +Este proyecto fue construido mientras se escuchaban estos álbumes: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) + [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 000000000..61230f353 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,144 @@ +

+
+

+ +

+ Créer des applications de bureau avec Go et les technologies Web. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Sommaire + +- [Sommaire](#sommaire) +- [Introduction](#introduction) +- [Fonctionnalités](#fonctionnalités) + - [Feuille de route](#feuille-de-route) +- [Démarrage](#démarrage) +- [Les sponsors](#les-sponsors) +- [Foire aux questions](#foire-aux-questions) +- [Les étoiles au fil du temps](#les-étoiles-au-fil-du-temps) +- [Les contributeurs](#les-contributeurs) +- [License](#license) +- [Inspiration](#inspiration) + +## Introduction + +La méthode traditionnelle pour fournir des interfaces web aux programmes Go consiste à utiliser un serveur web intégré. Wails propose une approche différente : il offre la possibilité d'intégrer à la fois le code Go et une interface web dans un seul binaire. Des outils sont fournis pour vous faciliter la tâche en gérant la création, la compilation et le regroupement des projets. Il ne vous reste plus qu'à faire preuve de créativité! + +## Fonctionnalités + +- Utiliser Go pour le backend +- Utilisez n'importe quelle technologie frontend avec laquelle vous êtes déjà familier pour construire votre interface utilisateur. +- Créez rapidement des interfaces riches pour vos programmes Go à l'aide de modèles prédéfinis. +- Appeler facilement des méthodes Go à partir de Javascript +- Définitions Typescript auto-générées pour vos structures et méthodes Go +- Dialogues et menus natifs +- Prise en charge native des modes sombre et clair +- Prise en charge des effets modernes de translucidité et de "frosted window". +- Système d'événements unifié entre Go et Javascript +- Outil puissant pour générer et construire rapidement vos projets +- Multiplateforme +- Utilise des moteurs de rendu natifs - _pas de navigateur intégré_ ! + +### Feuille de route + +La feuille de route du projet peut être consultée [ici](https://github.com/wailsapp/wails/discussions/1484). Veuillez consulter avant d'ouvrir une demande d'amélioration. + +## Démarrage + +Les instructions d'installation se trouvent sur le site [site officiel](https://wails.io/docs/gettingstarted/installation). + +## Les sponsors + +Ce projet est soutenu par ces personnes aimables et entreprises: + + +

+ +

+ +## Foire aux questions + +- S'agit-il d'une alternative à Electron ? + + Cela dépend de vos besoins. Il est conçu pour permettre aux programmeurs Go de créer facilement des applications de bureau légères ou d'ajouter une interface à leurs applications existantes. Wails offre des éléments natifs tels que des menus et des boîtes de dialogue, il peut donc être considéré comme une alternative légère à electron. + +- À qui s'adresse ce projet ? + + Les programmeurs Go qui souhaitent intégrer une interface HTML/JS/CSS à leurs applications, sans avoir à créer un serveur et à ouvrir un navigateur pour l'afficher. + +- Pourquoi ce nom ?? + + Lorsque j'ai vu WebView, je me suis dit : "Ce que je veux vraiment, c'est un outil pour construire une application WebView, un peu comme Rails l'est pour Ruby". Au départ, il s'agissait donc d'un jeu de mots (Webview on Rails). Il se trouve que c'est aussi un homophone du nom anglais du [Pays](https://en.wikipedia.org/wiki/Wales) d'où je viens. Il s'est donc imposé. + +## Les étoiles au fil du temps + +[![Graphique de l'histoire des étoiles](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## Les contributeurs + +La liste des contributeurs devient trop importante pour le readme ! Toutes les personnes extraordinaires qui ont contribué à ce projet ont leur propre page [ici](https://wails.io/credits#contributors). + +## License + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +Ce projet a été principalement codé sur les albums suivants : + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 000000000..ffd9f8103 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,152 @@ +

Wails

+ +

+
+

+ +

+ GoとWebの技術を用いてデスクトップアプリケーションを構築します。 +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## 目次 + +- [目次](#目次) +- [はじめに](#はじめに) +- [特徴](#特徴) + - [ロードマップ](#ロードマップ) +- [始め方](#始め方) +- [スポンサー](#スポンサー) +- [FAQ](#faq) +- [スター数の推移](#スター数の推移) +- [コントリビューター](#コントリビューター) +- [ライセンス](#ライセンス) +- [インスピレーション](#インスピレーション) + + +## はじめに + +Go プログラムにウェブインタフェースを提供する従来の方法は内蔵のウェブサーバを経由するものですが、 Wails では異なるアプローチを提供します。 +Wails では Go のコードとウェブフロントエンドを単一のバイナリにまとめる機能を提供します。 +また、プロジェクトの作成、コンパイル、ビルドを行うためのツールが提供されています。あなたがすべきことは創造性を発揮することです! + +## 特徴 + +- バックエンドには Go を利用しています +- 使い慣れたフロントエンド技術を利用して UI を構築できます +- あらかじめ用意されたテンプレートを利用することで、リッチなフロントエンドを備えた Go プログラムを素早く作成できます +- JavaScript から Go のメソッドを簡単に呼び出すことができます +- あなたの書いた Go の構造体やメソットに応じた TypeScript の定義が自動生成されます +- ネイティブのダイアログとメニューが利用できます +- ネイティブなダーク/ライトモードをサポートします +- モダンな半透明や「frosted window」エフェクトをサポートしています +- Go と JavaScript 間で統一されたイベント・システムを備えています +- プロジェクトを素早く生成して構築する強力な cli ツールを用意しています +- マルチプラットフォームに対応しています +- ネイティブなレンダリングエンジンを使用しています - _つまりブラウザを埋め込んでいるわけではありません!_ + +### ロードマップ + +プロジェクトのロードマップは[こちら](https://github.com/wailsapp/wails/discussions/1484)になります。 +機能拡張のリクエストを出す前にご覧ください。 + +## 始め方 + +インストール方法は[公式サイト](https://wails.io/docs/gettingstarted/installation)に掲載されています。 + +## スポンサー + +このプロジェクトは、以下の方々・企業によって支えられています。 + + +## FAQ + +- Electron の代替品になりますか? + + それはあなたの求める要件によります。Wails は Go プログラマーが簡単に軽量のデスクトップアプリケーションを作成したり、既存のアプリケーションにフロントエンドを追加できるように設計されています。 + Wails v2 ではメニューやダイアログといったネイティブな要素を提供するようになったため、軽量な Electron の代替となりつつあります。 + +- このプロジェクトは誰に向けたものですか? + + HTML/JS/CSS のフロントエンド技術をアプリケーションにバンドルさせることで、サーバーを作成してブラウザ経由で表示させることなくアプリケーションを利用したい Go プログラマにおすすめです。 + +- 名前の由来を教えて下さい + + WebView を見たとき、私はこう思いました。 + 「私が本当に欲しいのは、WebView アプリを構築するためのツールであり、Ruby に対する Rails のようなものである」と。 + そのため、最初は言葉遊びのつもりでした(Webview on Rails)。 + また、私の[出身国](https://en.wikipedia.org/wiki/Wales)の英語名と同音異義語でもあります。そしてこの名前が定着しました。 + +## スター数の推移 + +[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## コントリビューター + +貢献してくれた方のリストが大きくなりすぎて、readme に入りきらなくなりました! +このプロジェクトに貢献してくれた素晴らしい方々のページは[こちら](https://wails.io/credits#contributors)です。 + +## ライセンス + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## インスピレーション + +プロジェクトを進める際に、以下のアルバムたちも支えてくれています。 + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) + diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 000000000..075e04229 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,155 @@ +

Wails

+ +

+
+

+ +

+ Go & Web 기술을 사용하여 데스크탑 애플리케이션을 빌드하세요. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## 목차 + +- [목차](#목차) +- [소개](#소개) +- [기능](#기능) + - [로드맵](#로드맵) +- [시작하기](#시작하기) +- [스폰서](#스폰서) +- [FAQ](#faq) +- [Stargazers 성장 추세](#stargazers-성장-추세) +- [기여자](#기여자) +- [라이센스](#라이센스) +- [영감](#영감) + +## 소개 + +Go 프로그램에 웹 인터페이스를 제공하는 전통적인 방법은 내장 웹 서버를 이용하는 것입니다. +Wails는 다르게 접근합니다: Go 코드와 웹 프론트엔드를 단일 바이너리로 래핑하는 기능을 제공합니다. +프로젝트 생성, 컴파일 및 번들링을 처리하여 이를 쉽게 수행할 수 있도록 도구가 제공됩니다. +창의력을 발휘하기만 하면 됩니다! + +## 기능 + +- 백엔드에 표준 Go 사용 +- 이미 익숙한 프론트엔드 기술을 사용하여 UI 구축 +- 사전 구축된 템플릿을 사용하여 Go 프로그램을 위한 풍부한 프론트엔드를 빠르게 생성 +- Javascript에서 Go 메서드를 쉽게 호출 +- Go 구조체 및 메서드에 대한 자동 생성된 Typescript 정의 +- 기본 대화 및 메뉴 +- 네이티브 다크/라이트 모드 지원 +- 최신 반투명도 및 "반투명 창" 효과 지원 +- Go와 Javascript 간의 통합 이벤트 시스템 +- 프로젝트를 빠르게 생성하고 구축하는 강력한 CLI 도구 +- 멀티플랫폼 +- 기본 렌더링 엔진 사용 - _내장 브라우저 없음_! + +### 로드맵 + +프로젝트 로드맵은 [여기](https://github.com/wailsapp/wails/discussions/1484)에서 +확인할 수 있습니다. 개선 요청을 하기 전에 이것을 참조하십시오. + +## 시작하기 + +설치 지침은 +[공식 웹사이트](https://wails.io/docs/gettingstarted/installation)에 있습니다. + +## 스폰서 + +이 프로젝트는 친절한 사람들 / 회사들이 지원합니다. + + +## FAQ + +- 이것은 Electron의 대안인가요? + + 요구 사항에 따라 다릅니다. Go 프로그래머가 쉽게 가벼운 데스크톱 애플리케이션을 + 만들거나 기존 애플리케이션에 프론트엔드를 추가할 수 있도록 설계되었습니다. + Wails는 메뉴 및 대화 상자와 같은 기본 요소를 제공하므로 가벼운 Electron 대안으로 + 간주될 수 있습니다. + +- 이 프로젝트는 누구를 대상으로 하나요? + + 서버를 생성하고 이를 보기 위해 브라우저를 열 필요 없이 HTML/JS/CSS 프런트엔드를 + 애플리케이션과 함께 묶고자 하는 프로그래머를 대상으로 합니다. + +- Wails 이름의 의미는 무엇인가요? + + WebView를 보았을 때 저는 "내가 정말로 원하는 것은 WebView 앱을 구축하기 위한 + 도구를 사용하는거야. 마치 Ruby on Rails 처럼 말이야."라고 생각했습니다. + 그래서 처음에는 말장난(Webview on Rails)이었습니다. + [국가](https://en.wikipedia.org/wiki/Wales)에 대한 영어 이름의 동음이의어이기도 하여 정했습니다. + +## Stargazers 성장 추세 + +[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## 기여자 + +기여자 목록이 추가 정보에 비해 너무 커지고 있습니다! 이 프로젝트에 기여한 모든 놀라운 사람들은 +[여기](https://wails.io/credits#contributors)에 자신의 페이지를 가지고 있습니다. + +## 라이센스 + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## 영감 + +이 프로젝트는 주로 다음 앨범을 들으며 코딩되었습니다. + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.md b/README.md index 8276d9204..5ab9309b4 100644 --- a/README.md +++ b/README.md @@ -1 +1,159 @@ -# Coming Soon \ No newline at end of file +

+
+

+ +

+ Build desktop applications using Go & Web Technologies. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [Introduction](#introduction) +- [Features](#features) + - [Roadmap](#roadmap) +- [Getting Started](#getting-started) +- [Sponsors](#sponsors) +- [FAQ](#faq) +- [Stargazers over time](#stargazers-over-time) +- [Contributors](#contributors) +- [License](#license) +- [Inspiration](#inspiration) + +## Introduction + +The traditional method of providing web interfaces to Go programs is via a built-in web server. Wails offers a different +approach: it provides the ability to wrap both Go code and a web frontend into a single binary. Tools are provided to +make this easy for you by handling project creation, compilation and bundling. All you have to do is get creative! + +## Features + +- Use standard Go for the backend +- Use any frontend technology you are already familiar with to build your UI +- Quickly create rich frontends for your Go programs using pre-built templates +- Easily call Go methods from Javascript +- Auto-generated Typescript definitions for your Go structs and methods +- Native Dialogs & Menus +- Native Dark / Light mode support +- Supports modern translucency and "frosted window" effects +- Unified eventing system between Go and Javascript +- Powerful cli tool to quickly generate and build your projects +- Multiplatform +- Uses native rendering engines - _no embedded browser_! + +### Roadmap + +The project roadmap may be found [here](https://github.com/wailsapp/wails/discussions/1484). Please consult +it before creating an enhancement request. + +## Getting Started + +The installation instructions are on the [official website](https://wails.io/docs/gettingstarted/installation). + +## Sponsors + +This project is supported by these kind people / companies: + + +## Powered By + +[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) + +## FAQ + +- Is this an alternative to Electron? + + Depends on your requirements. It's designed to make it easy for Go programmers to make lightweight desktop + applications or add a frontend to their existing applications. Wails does offer native elements such as menus + and dialogs, so it could be considered a lightweight electron alternative. + +- Who is this project aimed at? + + Go programmers who want to bundle an HTML/JS/CSS frontend with their applications, without resorting to creating a + server and opening a browser to view it. + +- What's with the name? + + When I saw WebView, I thought "What I really want is tooling around building a WebView app, a bit like Rails is to + Ruby". So initially it was a play on words (Webview on Rails). It just so happened to also be a homophone of the + English name for the [Country](https://en.wikipedia.org/wiki/Wales) I am from. So it stuck. + +## Stargazers over time + + + + + + Star History Chart + + + +## Contributors + +The contributors list is getting too big for the readme! All the amazing people who have contributed to this +project have their own page [here](https://wails.io/credits#contributors). + +## License + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +This project was mainly coded to the following albums: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 000000000..0e3883352 --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,151 @@ +

+
+

+ +

+ Crie aplicativos de desktop usando Go e tecnologias Web. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Índice + +- [Índice](#índice) +- [Introdução](#introdução) +- [Recursos e funcionalidades](#recursos-e-funcionalidades) + - [Plano de trabalho](#plano-de-trabalho) +- [Iniciando](#iniciando) +- [Patrocinadores](#patrocinadores) +- [Perguntas frequentes](#perguntas-frequentes) +- [Estrelas ao longo do tempo](#estrelas-ao-longo-do-tempo) +- [Colaboradores](#colaboradores) +- [Licença](#licença) +- [Inspiração](#inspiração) + +## Introdução + +O método tradicional de fornecer interfaces da Web para programas Go é por meio de um servidor da Web integrado. Wails oferece uma +abordagem: fornece a capacidade de agrupar o código Go e um front-end da Web em um único binário. As ferramentas são fornecidas para +que torne isso mais fácil para você lidando com a criação, compilação e agrupamento de projetos. Tudo o que você precisa fazer é ser criativo! + +## Recursos e funcionalidades + +- Use Go padrão para o back-end +- Use qualquer tecnologia de front-end com a qual você já esteja familiarizado para criar sua interface do usuário +- Crie rapidamente um front-end avançado para seus programas Go usando modelos pré-construídos +- Chame facilmente métodos Go com JavaScript +- Definições TypeScript geradas automaticamente para suas estruturas e métodos Go +- Diálogos e menus nativos +- Suporte nativo ao modo escuro/claro +- Suporta translucidez moderna e efeitos de "janela fosca" +- Sistema de eventos unificado entre Go e JavaScript +- Poderosa ferramenta cli para gerar e construir rapidamente seus projetos +- Multiplataforma +- Usa mecanismos de renderização nativos - _sem navegador incorporado_! + +### Plano de trabalho + +O plano de trabalho do projeto pode ser encontrado [aqui](https://github.com/wailsapp/wails/discussions/1484). Por favor consulte +isso antes de abrir um pedido de melhoria. + +## Iniciando + +As instruções de instalação estão no [site oficial](https://wails.io/docs/gettingstarted/installation). + +## Patrocinadores + +Este projeto é apoiado por estas simpáticas pessoas/empresas: + + +

+ +

+ +## Perguntas frequentes + +- Esta é uma alternativa ao Electron? + + Depende de seus requisitos. Ele foi projetado para tornar mais fácil para os programadores Go criar aplicações desktop + e adicionar um front-end aos seus aplicativos existentes. O Wails oferece elementos nativos, como menus + e diálogos, por isso pode ser considerada uma alternativa leve, se comparado ao Electron. + +- A quem se destina este projeto? + + Programadores Go que desejam agrupar um front-end HTML/JS/CSS com seus aplicativos, sem recorrer à criação de um + servidor e abrir um navegador para visualizá-lo. + +- Qual é o significado do nome? + + Quando vi o WebView, pensei "O que eu realmente quero é ferramentas para construir um aplicativo WebView, algo semelhante ao que Rails é para Ruby". Portanto, inicialmente era um jogo de palavras (WebView on Rails). Por acaso, também era um homófono do + Nome em inglês para o [país](https://en.wikipedia.org/wiki/Wales) de onde eu sou. Então ficou com esse nome. + +## Estrelas ao longo do tempo + +[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## Colaboradores + +A lista de colaboradores está ficando grande demais para o arquivo readme! Todas as pessoas incríveis que contribuíram para o +projeto tem sua própria página [aqui](https://wails.io/credits#contributors). + +## Licença + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiração + +Este projeto foi construído ouvindo esses álbuns: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 000000000..76fa59d07 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,153 @@ +

+
+

+ +

+ Собирайте Desktop приложения используя Go и Web технологии +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Содержание + +- [Содержание](#содержание) +- [Вступление](#вступление) +- [Особенности](#особенности) + - [Roadmap](#roadmap) +- [Быстрый старт](#быстрый-старт) +- [Спонсоры](#спонсоры) +- [FAQ](#faq) +- [График звёздочек](#график-звёздочек-репозитория-относительно-времени) +- [Контребьюторы](#контребьюторы) +- [Лицензия](#лицензия) +- [Вдохновение](#вдохновение) + +## Вступление + +Обычно, веб-интерфейсы для программ Go - это встроенный веб-сервер и веб-браузер. +У Walls другой подход: он оборачивает как код Go, так и веб-интерфейс в один бинарник (EXE файл). +Облегчает вам создание вашего приложения, управляя созданием, компиляцией и объединением проектов. +Все ограничивается лишь вашей фантазией! + +## Особенности + +- Использование Go для backend +- Поддержка любой frontend технологии, с которой вы уже знакомы для создания вашего UI +- Быстрое создание frontend для ваших программ, используя готовые шаблоны +- Очень лёгкий вызов функций Go из JavaScript +- Автогенерация TypeScript типов для Go структур и функций +- Нативные диалоги и меню +- Нативная поддержка тёмной и светлой темы +- Поддержка современных эффектов прозрачности и "матового окна" +- Единая система эвентов для Go и JavaScript +- Мощный CLI для быстрого создания ваших проектов +- Мультиплатформенность +- Использование нативного движка рендеринга - нет встроенному браузеру! + +### Roadmap + +Roadmap проекта вы можете найти [здесь](https://github.com/wailsapp/wails/discussions/1484). +Пожалуйста, проконсультируйтесь перед предложением улучшения. + +## Быстрый старт + +Инструкции по установке находятся на [официальном сайте](https://wails.io/docs/gettingstarted/installation). + +## Спонсоры + +Проект поддерживается этими добрыми людьми / компаниями: + + +

+ +

+ +## FAQ + +- Это альтернатива Electron? + + Зависит от ваших требований. Wails разработан для легкого создания Desktop приложений или + расширения интерфейсной части существующих приложений для программистов на Go. Wails действительно + предлагает встроенные элементы, такие как меню и диалоги, так что его можно считать облегченной альтернативой Electron. + +- Для кого предназначен этот проект? + + Для Golang программистов, которые хотят создавать приложения, используя HTML, JS и CSS, + без создания веб-сервера и открытия браузера для их просмотра. + +- Что это за название? + + Когда я увидел WebView, я подумал: "Что мне действительно нужно, так это инструменты для создания приложения WebView, + немного похожие на Rails для Ruby". Изначально это была игра слов (Webview on Rails). Просто так получилось, что это + также омофон английского названия для [Страны](https://en.wikipedia.org/wiki/Wales) от куда я родом. Так что это прижилось. + +## График звёздочек репозитория по времени + +[![График звёзд](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## Контрибьюторы + +Список участников слишком велик для README! У всех замечательных людей, которые внесли свой вклад в этот +проект, есть своя [страничка](https://wails.io/credits#contributors). + +## Лицензия + +[![Статус FOSSA](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Вдохновение + +Этот проект был создан, в основном, под эти альбомы: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 000000000..e9b16ca76 --- /dev/null +++ b/README.tr.md @@ -0,0 +1,156 @@ +

+
+

+ +

+ Go ve Web Teknolojilerini kullanarak masaüstü uygulamaları oluşturun. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · +[Türkçe](README.tr.md) + + + +
+ +## İçerik + +- [İçerik](#içerik) +- [Giriş](#giriş) +- [Özellikler](#özellikler) + - [Yol Haritası](#yol-haritası) +- [Başlarken](#başlarken) +- [Sponsorlar](#sponsorlar) +- [Sıkça sorulan sorular](#sıkça-sorulan-sorular) +- [Zaman içinda yıldızlayanlar](#zaman-içinde-yıldızlayanlar) +- [Katkıda bulunanlar](#katkıda-bulunanlar) +- [Lisans](#lisans) +- [İlham](#ilham) + +## Giriş + +Go programlarına web arayüzleri sağlamak için geleneksel yöntem, yerleşik bir web sunucusu kullanmaktır. Wails, farklı bir yaklaşım sunar: Hem Go kodunu hem de bir web ön yüzünü tek bir ikili dosyada paketleme yeteneği sağlar. Proje oluşturma, derleme ve paketleme işlemlerini kolaylaştıran araçlar sunar. Tek yapmanız gereken yaratıcı olmaktır! + +## Özellikler + +- Backend için standart Go kullanın +- Kullanıcı arayüzünüzü oluşturmak için zaten aşina olduğunuz herhangi bir frontend teknolojisini kullanın +- Hazır şablonlar kullanarak Go programlarınız için hızlıca zengin ön yüzler oluşturun +- Javascript'ten Go metodlarını kolayca çağırın +- Go yapı ve metodlarınız için otomatik oluşturulan Typescript tanımları +- Yerel Diyaloglar ve Menüler +- Yerel Karanlık / Aydınlık mod desteği +- Modern saydamlık ve "buzlu cam" efektlerini destekler +- Go ve Javascript arasında birleşik olay sistemi +- Projelerinizi hızlıca oluşturmak ve derlemek için güçlü bir komut satırı aracı +- Çoklu platform desteği +- Yerel render motorlarını kullanır - _gömülü tarayıcı yok_! + + +### Yol Haritesı + +Proje yol haritasına [buradan](https://github.com/wailsapp/wails/discussions/1484) ulaşabilirsiniz. Lütfen bir iyileştirme talebi oluşturmadan önce danışın. + + +## Başlarken + +Kurulum talimatları [resmi web sitesinde](https://wails.io/docs/gettingstarted/installation) bulunmaktadır. + + +## Sponsorlar + +Bu proje, aşağıdaki nazik insanlar / şirketler tarafından desteklenmektedir: + + +

+ +

+ +## Sıkça Sorulan Sorular + +- Bu Electron'a alternatif mi? + + Gereksinimlerinize bağlıdır. Go programcılarının hafif masaüstü uygulamaları yapmasını veya mevcut uygulamalarına bir ön yüz eklemelerini kolaylaştırmak için tasarlanmıştır. Wails, menüler ve diyaloglar gibi yerel öğeler sunduğundan, hafif bir Electron alternatifi olarak kabul edilebilir. + +- Bu proje kimlere yöneliktir? + + HTML/JS/CSS ön yüzünü uygulamalarıyla birlikte paketlemek isteyen, ancak bir sunucu oluşturup bir tarayıcı açmaya başvurmadan bunu yapmak isteyen Go programcıları için. + +- İsmin anlamı nedir? + + WebView'i gördüğümde, "Aslında istediğim şey, WebView uygulaması oluşturmak için araçlar, biraz Rails'in Ruby için olduğu gibi" diye düşündüm. Bu nedenle başlangıçta kelime oyunu (Rails üzerinde Webview) olarak ortaya çıktı. Ayrıca, benim geldiğim [ülkenin](https://en.wikipedia.org/wiki/Wales) İngilizce adıyla homofon olması tesadüf oldu. Bu yüzden bu isim kaldı. + + +## Zaman içinda yıldızlayanlar + + + + + + Star History Chart + + + +## Katkıda Bulunanlar + +Katkıda bulunanların listesi, README için çok büyük hale geldi! Bu projeye katkıda bulunan tüm harika insanların kendi sayfaları [burada](https://wails.io/credits#contributors) bulunmaktadır. + + +## Lisans + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## İlham + +Bu proje esas olarak aşağıdaki albümler dinlenilerek kodlandı: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) + diff --git a/README.uz.md b/README.uz.md new file mode 100644 index 000000000..807262405 --- /dev/null +++ b/README.uz.md @@ -0,0 +1,159 @@ +

+
+

+ +

+ Go va Web texnologiyalaridan foydalangan holda ish stoli ilovalarini yarating +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Tarkib + +- [Tarkib](#tarkib) +- [Kirish](#kirish) +- [Xususiyatlari](#xususiyatlari) + - [Yo'l xaritasi](#yol-xaritasi) +- [Ishni boshlash](#ishni-boshlash) +- [Homiylar](#homiylar) +- [FAQ](#faq) +- [Vaqt o'tishi bilan yulduzlar](#vaqt-otishi-bilan-yulduzlar) +- [Ishtirokchilar](#homiylar) +- [Litsenziya](#litsenziya) +- [Ilhomlanish](#ilhomlanish) + +## Kirish + +Odatda, Go dasturlari uchun veb-interfeyslar o'rnatilgan veb-server va veb-brauzerdir. +Walls boshqacha yondashuvni qo'llaydi: u Go kodini ham, veb-interfeysni ham bitta ikkilik (e.g: EXE)fayliga o'raydi. +Loyihalarni yaratish, kompilyatsiya qilish va birlashtirishni boshqarish orqali ilovangizni yaratishni osonlashtiradi. +Hamma narsa faqat sizning tasavvuringiz bilan cheklangan! + +## Xususiyatlari + +- Backend uchun standart Go dan foydalaning +- UI yaratish uchun siz allaqachon tanish bo'lgan har qanday frontend texnologiyasidan foydalaning +- Oldindan tayyorlangan shablonlardan foydalanib, Go dasturlaringiz uchun tezda boy frontendlarni yarating +- Javascriptdan Go methodlarini osongina chaqiring +- Go struktura va methodlari uchun avtomatik yaratilgan Typescript ta'riflari +- Mahalliy Dialoglar va Menyular +- Mahalliy Dark / Light rejimini qo'llab-quvvatlash +- Zamonaviy shaffoflik va "muzli oyna" effektlarini qo'llab-quvvatlaydi +- Go va Javascript o'rtasidagi yagona hodisa tizimi +- Loyihalaringizni tezda yaratish va qurish uchun kuchli cli vositasi +- Ko'p platformali +- Mahalliy renderlash mexanizmlaridan foydalanadi - _o'rnatilgan brauzer yo'q_! + +### Yo'l xaritasi + +Loyihaning yoʻl xaritasini [bu yerdan](https://github.com/wailsapp/wails/discussions/1484) topish mumkin. Iltimos, maslahatlashing +Buni yaxshilash so'rovini ochishdan oldin. + +## Ishni boshlash + +O'rnatish bo'yicha ko'rsatmalar [Rasmiy veb saytda](https://wails.io/docs/gettingstarted/installation) mavjud. + +## Homiylar + +Ushbu loyiha quyidagi mehribon odamlar / kompaniyalar tomonidan qo'llab-quvvatlanadi: + + +

+ +

+ +## FAQ + +- Bu Elektronga muqobilmi? + + Sizning talablaringizga bog'liq. Bu Go dasturchilariga yengil ish stoli yaratishni osonlashtirish uchun yaratilgan + ilovalar yoki ularning mavjud ilovalariga frontend qo'shing. Wails menyular kabi mahalliy elementlarni taklif qiladi + va dialoglar, shuning uchun uni yengil elektron muqobili deb hisoblash mumkin. + +- Ushbu loyiha kimlar uchun? + + Server yaratmasdan va uni ko'rish uchun brauzerni ochmasdan, o'z ilovalari bilan HTML/JS/CSS orqali frontendini birlashtirmoqchi bo'lgan dasturchilar uchun. + +- Bu qanday nom? + + Men WebViewni ko'rganimda, men shunday deb o'yladim: "Menga WebView ilovasini yaratish uchun vositalar kerak. + biroz Rails for Rubyga o'xshaydi." Demak, dastlab bu so'zlar ustida o'yin edi (Railsda Webview). Shunday bo'ldi. + u men kelgan [Mamlakat](https://en.wikipedia.org/wiki/Wales)ning inglizcha nomining omofonidir. + +## Vaqt o'tishi bilan yulduzlar + + + + + + Yulduzlar tarixi jadvali + + + +## Ishtirokchilar + +Ishtirokchilar roʻyxati oʻqish uchun juda kattalashib bormoqda! Bunga hissa qo'shgan barcha ajoyib odamlarning +loyihada o'z sahifasi bor [bu yerga](https://wails.io/credits#contributors). + +## Litsenziya + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Ilhomlanish + +Ushbu loyiha asosan quyidagi albomlar uchun kodlangan: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.zh-Hans.md b/README.zh-Hans.md new file mode 100644 index 000000000..4c09d0c45 --- /dev/null +++ b/README.zh-Hans.md @@ -0,0 +1,144 @@ +

Wails

+ +

+
+

+ +

+ 使用 Go 和 Web 技术构建桌面应用程序。 +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## 内容目录 + +- [内容目录](#内容目录) +- [项目介绍](#项目介绍) +- [功能](#功能) + - [路线图](#路线图) +- [快速入门](#快速入门) +- [赞助商](#赞助商) +- [常见问题](#常见问题) +- [星星增长趋势](#星星增长趋势) +- [贡献者](#贡献者) +- [许可证](#许可证) +- [灵感](#灵感) + +## 项目介绍 + +为 Go 程序提供 Web 界面的传统方法是通过内置 Web 服务器。Wails 提供了一种不同的方法:它提供了将 Go 代码和 Web +前端一起打包成单个二进制文件的能力。通过提供的工具,可以很轻松的完成项目的创建、编译和打包。你所要做的就是发挥创造力! + +## 功能 + +- 后端使用标准 Go +- 使用您已经熟悉的任何前端技术来构建您的 UI +- 使用内置模板为您的 Go 程序快速创建丰富的前端 +- 从 Javascript 轻松调用 Go 方法 +- 为您的 Go 结构体和方法自动生成 Typescript 声明 +- 原生对话框和菜单 +- 支持现代半透明和“磨砂窗”效果 +- Go 和 Javascript 之间统一的事件系统 +- 强大的命令行工具,可快速生成和构建您的项目 +- 跨平台 +- 使用原生渲染引擎 - _没有嵌入浏览器_! + +### 路线图 + +项目路线图可在 [此处](https://github.com/wailsapp/wails/discussions/1484) 找到。在提出增强请求之前请查阅此内容。 + +## 快速入门 + +使用说明在 [官网](https://wails.io/zh-Hans/docs/gettingstarted/installation/)。 + +## 赞助商 + +这个项目由以下这些人或者公司支持: + + + +## 常见问题 + +- 它是 Electron 的替代品吗? + + 取决于您的要求。它旨在使 Go 程序员可以轻松制作轻量级桌面应用程序或在其现有应用程序中添加前端。尽管 Wails 当前不提供对诸如菜单之类的原生元素的钩子,但将来可能会改变。 + +- 这个项目针对的是哪些人? + + 希望将 HTML / JS / CSS 前端与其应用程序捆绑在一起的程序员,而不是借助创建服务并打开浏览器进行查看的方式。 + +- 名字怎么来的? + + 当我看到 WebView 时,我想"我真正想要的是围绕构建 WebView 应用程序工作,有点像 Rails 对于 Ruby"。因此,最初它是一个文字游戏(Webview on + Rails)。碰巧也是我来自的 [国家](https://en.wikipedia.org/wiki/Wales) 的英文名字的同音。所以就是它了。 + +## 星星增长趋势 + +[![星星增长趋势](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## 贡献者 + +贡献者列表对于 README 文件来说太大了!所有为这个项目做出贡献的了不起的人在[这里](https://wails.io/credits#contributors)都有自己的页面。 + +## 许可证 + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## 灵感 + +项目灵感主要来自以下专辑: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cb096f872 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x.x | :white_check_mark: | +| 3.0.x-alpha | :x: | + + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in our project, we encourage you to let us know right away. +We will investigate all legitimate reports and do our best to quickly fix the problem. + +Before reporting though, please review our security policy below. + +### How to Report + +To report a security vulnerability, please use GitHub's [private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) feature. If possible, please include as much information as possible. +This may include steps to reproduce, impact of the vulnerability, and anything else you believe would help us understand the problem. +**Please do not include any sensitive or personal information in your report**. + +### What to Expect + +When you report a vulnerability, here's what you can expect: + +- **Acknowledgement**: We will acknowledge your email within 48 hours, and you'll receive a more detailed response to your email within 72 hours indicating the next steps in handling your report. + +- **Updates**: After the initial reply to your report, our team will keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least once a week. + +- **Confidentiality**: We will maintain strict confidentiality of your report until the security issue is resolved. + +- **Issue Resolution**: If the issue is confirmed, we will release a patch as soon as possible depending on complexity of the fix. + +- **Recognition**: We recognize and appreciate every individual who helps us identify and fix vulnerabilities in our project. While we do not currently have a bounty program, we would be happy to publicly acknowledge your responsible disclosure. + +We strive to make Wails safe for everyone, and we greatly appreciate the assistance of security researchers and users in helping us identify and fix vulnerabilities. Thank you for your contribution to the security of this project. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 000000000..7cc165825 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,47 @@ +# https://taskfile.dev + +version: "3" + +includes: + website: + taskfile: website + dir: website + + v2: + taskfile: v2 + dir: v2 + optional: true + v3: + taskfile: v3 + dir: v3 + optional: true + +tasks: + contributors:check: + cmds: + - npx -y all-contributors-cli check + + contributors:update: + cmds: + - go run v3/tasks/contribs/main.go + + contributors:build: + cmds: + - npx -y all-contributors-cli generate + + format:md: + cmds: + - npx prettier --write "**/*.md" + + format: + cmds: + - task: format:md + + format-all-md: + cmds: + - task: format:md + - task: website:format:md + - task: v2:format:md + # - task: v2:website:format + - task: v3:format:md + # - task: v3:website:format:md diff --git a/app.go b/app.go deleted file mode 100644 index 09f00f3de..000000000 --- a/app.go +++ /dev/null @@ -1,138 +0,0 @@ -package wails - -import ( - "github.com/wailsapp/wails/cmd" -) - -// -------------------------------- Compile time Flags ------------------------------ - -// BuildMode indicates what mode we are in -var BuildMode = cmd.BuildModeProd - -// ---------------------------------------------------------------------------------- - -// App defines the main application struct -type App struct { - config *AppConfig // The Application configuration object - cli *cmd.Cli // In debug mode, we have a cli - renderer Renderer // The renderer is what we will render the app to - logLevel string // The log level of the app - ipc *ipcManager // Handles the IPC calls - log *CustomLogger // Logger - bindingManager *bindingManager // Handles binding of Go code to renderer - eventManager *eventManager // Handles all the events - runtime *Runtime // The runtime object for registered structs - - // This is a list of all the JS/CSS that needs injecting - // It will get injected in order - jsCache []string - cssCache []string -} - -// CreateApp creates the application window with the given configuration -// If none given, the defaults are used -func CreateApp(optionalConfig ...*AppConfig) *App { - var userConfig *AppConfig - if len(optionalConfig) > 0 { - userConfig = optionalConfig[0] - } - - result := &App{ - logLevel: "info", - renderer: &webViewRenderer{}, - ipc: newIPCManager(), - bindingManager: newBindingManager(), - eventManager: newEventManager(), - log: newCustomLogger("App"), - } - - appconfig, err := newAppConfig(userConfig) - if err != nil { - result.log.Fatalf("Cannot use custom HTML: %s", err.Error()) - } - result.config = appconfig - - // Set up the CLI if not in release mode - if BuildMode != cmd.BuildModeProd { - result.cli = result.setupCli() - } else { - // Disable Inspector in release mode - result.config.DisableInspector = true - } - - return result -} - -// Run the app -func (a *App) Run() error { - if BuildMode != cmd.BuildModeProd { - return a.cli.Run() - } - - a.logLevel = "error" - return a.start() -} - -func (a *App) start() error { - - // Set the log level - setLogLevel(a.logLevel) - - // Log starup - a.log.Info("Starting") - - // Check if we are to run in headless mode - if BuildMode == cmd.BuildModeBridge { - a.renderer = &Headless{} - } - - // Initialise the renderer - err := a.renderer.Initialise(a.config, a.ipc, a.eventManager) - if err != nil { - return err - } - - // Start event manager and give it our renderer - a.eventManager.start(a.renderer) - - // Start the IPC Manager and give it the event manager and binding manager - a.ipc.start(a.eventManager, a.bindingManager) - - // Create the runtime - a.runtime = newRuntime(a.eventManager, a.renderer) - - // Start binding manager and give it our renderer - err = a.bindingManager.start(a.renderer, a.runtime) - if err != nil { - return err - } - - // Inject CSS - a.renderer.AddCSSList(a.cssCache) - - // Inject JS - a.renderer.AddJSList(a.jsCache) - - // Run the renderer - a.renderer.Run() - - return nil -} - -// Bind allows the user to bind the given object -// with the application -func (a *App) Bind(object interface{}) { - a.bindingManager.bind(object) -} - -// AddJS adds a piece of Javascript to a cache that -// gets injected at runtime -func (a *App) AddJS(js string) { - a.jsCache = append(a.jsCache, js) -} - -// AddCSS adds a CSS string to a cache that -// gets injected at runtime -func (a *App) AddCSS(js string) { - a.cssCache = append(a.cssCache, js) -} diff --git a/app_cli.go b/app_cli.go deleted file mode 100644 index b711c3473..000000000 --- a/app_cli.go +++ /dev/null @@ -1,31 +0,0 @@ -package wails - -import ( - "fmt" - - "github.com/wailsapp/wails/cmd" -) - -// setupCli creates a new cli handler for the application -func (app *App) setupCli() *cmd.Cli { - - // Create a new cli - result := cmd.NewCli(app.config.Title, "Debug build") - - // Setup cli to handle loglevel and headless flags - result. - StringFlag("loglevel", "Sets the log level [debug|info|error|panic|fatal]. Default debug", &app.logLevel). - // BoolFlag("headless", "Runs the app in headless mode", &app.headless). - Action(app.start) - - // Banner - result.PreRun(func(cli *cmd.Cli) error { - log := cmd.NewLogger() - log.PrintSmallBanner() - fmt.Println() - log.YellowUnderline(app.config.Title + " - Debug Build") - return nil - }) - - return result -} diff --git a/app_config.go b/app_config.go deleted file mode 100644 index ed5579e7a..000000000 --- a/app_config.go +++ /dev/null @@ -1,97 +0,0 @@ -package wails - -import ( - "strings" - - "github.com/dchest/htmlmin" - "github.com/leaanthony/mewn" -) - -// AppConfig is the configuration structure used when creating a Wails App object -type AppConfig struct { - Width, Height int - Title string - defaultHTML string - HTML string - JS string - CSS string - Colour string - Resizable bool - DisableInspector bool - isHTMLFragment bool -} - -func (a *AppConfig) merge(in *AppConfig) error { - if in.CSS != "" { - a.CSS = in.CSS - } - if in.Title != "" { - a.Title = in.Title - } - if in.HTML != "" { - minified, err := htmlmin.Minify([]byte(in.HTML), &htmlmin.Options{ - MinifyScripts: true, - }) - if err != nil { - return err - } - inlineHTML := string(minified) - inlineHTML = strings.Replace(inlineHTML, "'", "\\'", -1) - inlineHTML = strings.Replace(inlineHTML, "\n", " ", -1) - a.HTML = strings.TrimSpace(inlineHTML) - - // Deduce whether this is a full html page or a fragment - // The document is determined to be a fragment if an HMTL - // tag exists and is located before the first div tag - HTMLTagIndex := strings.Index(a.HTML, " 0 { - b.inputs = make([]reflect.Type, inputParamCount) - // We start at 1 as the first param is the struct - for index := 0; index < inputParamCount; index++ { - param := functionType.In(index) - name := param.Name() - kind := param.Kind() - b.inputs[index] = param - typ := param - index := index - b.log.DebugFields("Input param", Fields{ - "index": index, - "name": name, - "kind": kind, - "typ": typ, - }) - } - } - - // Process return/output declarations - returnParamsCount := functionType.NumOut() - // Guard against bad number of return types - switch returnParamsCount { - case 0: - case 1: - // Check if it's an error type - param := functionType.Out(0) - paramName := param.Name() - if paramName == "error" { - b.hasErrorReturnType = true - } - // Save return type - b.returnTypes = append(b.returnTypes, param) - case 2: - // Check the second return type is an error - secondParam := functionType.Out(1) - secondParamName := secondParam.Name() - if secondParamName != "error" { - return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.fullName, secondParamName) - } - - // Check the second return type is an error - firstParam := functionType.Out(0) - firstParamName := firstParam.Name() - if firstParamName == "error" { - return fmt.Errorf("first return type of method '%s' must not be an error", b.fullName) - } - b.hasErrorReturnType = true - - // Save return types - b.returnTypes = append(b.returnTypes, firstParam) - b.returnTypes = append(b.returnTypes, secondParam) - - default: - return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.fullName, returnParamsCount) - } - - return nil -} - -// call the method with the given data -func (b *boundFunction) call(data string) ([]reflect.Value, error) { - - // The data will be an array of values so we will decode the - // input data into - var jsArgs []interface{} - d := json.NewDecoder(bytes.NewBufferString(data)) - // d.UseNumber() - err := d.Decode(&jsArgs) - if err != nil { - return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error()) - } - - // Check correct number of inputs - if len(jsArgs) != len(b.inputs) { - return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs)) - } - - // Set up call - args := make([]reflect.Value, len(b.inputs)) - for index := 0; index < len(b.inputs); index++ { - - // Set the input values - value, err := b.setInputValue(index, b.inputs[index], jsArgs[index]) - if err != nil { - return nil, err - } - args[index] = value - } - b.log.Debugf("Unmarshalled Args: %+v\n", jsArgs) - b.log.Debugf("Converted Args: %+v\n", args) - results := b.function.Call(args) - - b.log.Debugf("results = %+v", results) - return results, nil -} - -// Attempts to set the method input for parameter with the given value -func (b *boundFunction) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) { - - // Catch type conversion panics thrown by convert - defer func() { - if r := recover(); r != nil { - // Modify error - err = fmt.Errorf("%s for parameter %d of function %s", r.(string)[23:], index+1, b.fullName) - } - }() - - // Do the conversion - result = reflect.ValueOf(val).Convert(typ) - - return result, err -} diff --git a/binding_manager.go b/binding_manager.go deleted file mode 100644 index f6a04f8f9..000000000 --- a/binding_manager.go +++ /dev/null @@ -1,282 +0,0 @@ -package wails - -import ( - "fmt" - "reflect" - "unicode" -) - -/** - -binding: - Name() // Full name (package+name) - Call(params) - -**/ - -type bindingManager struct { - methods map[string]*boundMethod - functions map[string]*boundFunction - initMethods []*boundMethod - log *CustomLogger - renderer Renderer - runtime *Runtime // The runtime object to pass to bound structs - objectsToBind []interface{} - bindPackageNames bool // Package name should be considered when binding -} - -func newBindingManager() *bindingManager { - result := &bindingManager{ - methods: make(map[string]*boundMethod), - functions: make(map[string]*boundFunction), - log: newCustomLogger("Bind"), - } - return result -} - -// Sets flag to indicate package names should be considered when binding -func (b *bindingManager) BindPackageNames() { - b.bindPackageNames = true -} - -func (b *bindingManager) start(renderer Renderer, runtime *Runtime) error { - b.log.Info("Starting") - b.renderer = renderer - b.runtime = runtime - err := b.initialise() - if err != nil { - b.log.Errorf("Binding error: %s", err.Error()) - return err - } - err = b.callWailsInitMethods() - return err -} - -func (b *bindingManager) initialise() error { - - var err error - // var binding *boundMethod - - b.log.Info("Binding Go Functions/Methods") - - // Create bindings for objects - for _, object := range b.objectsToBind { - - // Safeguard against nils - if object == nil { - return fmt.Errorf("attempted to bind nil object") - } - - // Determine kind of object - objectType := reflect.TypeOf(object) - objectKind := objectType.Kind() - - switch objectKind { - case reflect.Ptr: - err = b.bindMethod(object) - case reflect.Func: - // spew.Dump(result.objectType.String()) - err = b.bindFunction(object) - default: - err = fmt.Errorf("cannot bind object of type '%s'", objectKind.String()) - } - - // Return error if set - if err != nil { - return err - } - } - return nil -} - -// bind the given struct method -func (b *bindingManager) bindMethod(object interface{}) error { - - objectType := reflect.TypeOf(object) - baseName := objectType.String() - - // Strip pointer if there - if baseName[0] == '*' { - baseName = baseName[1:] - } - - b.log.Debugf("Processing struct: %s", baseName) - - // Iterate over method definitions - for i := 0; i < objectType.NumMethod(); i++ { - - // Get method definition - methodDef := objectType.Method(i) - methodName := methodDef.Name - fullMethodName := baseName + "." + methodName - method := reflect.ValueOf(object).MethodByName(methodName) - - // Skip unexported methods - if !unicode.IsUpper([]rune(methodName)[0]) { - continue - } - - // Create a new boundMethod - newMethod, err := newBoundMethod(methodName, fullMethodName, method, objectType) - if err != nil { - return err - } - - // Check if it's a wails init function - if newMethod.isWailsInit { - b.log.Debugf("Detected WailsInit function: %s", fullMethodName) - b.initMethods = append(b.initMethods, newMethod) - } else { - // Save boundMethod - b.log.Infof("Bound Method: %s()", fullMethodName) - b.methods[fullMethodName] = newMethod - - // Inform renderer of new binding - b.renderer.NewBinding(fullMethodName) - } - } - - return nil -} - -// bind the given function object -func (b *bindingManager) bindFunction(object interface{}) error { - - newFunction, err := newBoundFunction(object) - if err != nil { - return err - } - - // Save method - b.log.Infof("Bound Function: %s()", newFunction.fullName) - b.functions[newFunction.fullName] = newFunction - - // Register with Renderer - b.renderer.NewBinding(newFunction.fullName) - - return nil -} - -// Save the given object to be bound at start time -func (b *bindingManager) bind(object interface{}) { - // Store binding - b.objectsToBind = append(b.objectsToBind, object) -} - -func (b *bindingManager) processFunctionCall(callData *callData) (interface{}, error) { - // Return values - var result []reflect.Value - var err error - - function := b.functions[callData.BindingName] - if function == nil { - return nil, fmt.Errorf("Invalid function name '%s'", callData.BindingName) - } - result, err = function.call(callData.Data) - if err != nil { - return nil, err - } - - // Do we have an error return type? - if function.hasErrorReturnType { - // We do - last result is an error type - // Check if the last result was nil - b.log.Debugf("# of return types: %d", len(function.returnTypes)) - b.log.Debugf("# of results: %d", len(result)) - errorResult := result[len(function.returnTypes)-1] - if !errorResult.IsNil() { - // It wasn't - we have an error - return nil, errorResult.Interface().(error) - } - } - return result[0].Interface(), nil -} - -func (b *bindingManager) processMethodCall(callData *callData) (interface{}, error) { - // Return values - var result []reflect.Value - var err error - - // do we have this method? - method := b.methods[callData.BindingName] - if method == nil { - return nil, fmt.Errorf("Invalid method name '%s'", callData.BindingName) - } - - result, err = method.call(callData.Data) - if err != nil { - return nil, err - } - - // Do we have an error return type? - if method.hasErrorReturnType { - // We do - last result is an error type - // Check if the last result was nil - b.log.Debugf("# of return types: %d", len(method.returnTypes)) - b.log.Debugf("# of results: %d", len(result)) - errorResult := result[len(method.returnTypes)-1] - if !errorResult.IsNil() { - // It wasn't - we have an error - return nil, errorResult.Interface().(error) - } - } - if result != nil { - return result[0].Interface(), nil - } - return nil, nil -} - -// process an incoming call request -func (b *bindingManager) processCall(callData *callData) (result interface{}, err error) { - b.log.Debugf("Wanting to call %s", callData.BindingName) - - // Determine if this is function call or method call by the number of - // dots in the binding name - dotCount := 0 - for _, character := range callData.BindingName { - if character == '.' { - dotCount++ - } - } - - // We need to catch reflect related panics and return - // a decent error message - // TODO: DEBUG THIS! - - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("%s", r.(string)) - } - }() - - switch dotCount { - case 1: - result, err = b.processFunctionCall(callData) - case 2: - result, err = b.processMethodCall(callData) - default: - result = nil - err = fmt.Errorf("Invalid binding name '%s'", callData.BindingName) - } - return -} - -// callWailsInitMethods calls all of the WailsInit methods that were -// registered with the runtime object -func (b *bindingManager) callWailsInitMethods() error { - // Create reflect value for runtime object - runtimeValue := reflect.ValueOf(b.runtime) - params := []reflect.Value{runtimeValue} - - // Iterate initMethods - for _, initMethod := range b.initMethods { - // Call - result := initMethod.method.Call(params) - // Check errors - err := result[0].Interface() - if err != nil { - return err.(error) - } - } - return nil -} diff --git a/binding_method.go b/binding_method.go deleted file mode 100644 index a8245eee3..000000000 --- a/binding_method.go +++ /dev/null @@ -1,211 +0,0 @@ -package wails - -import ( - "bytes" - "encoding/json" - "fmt" - "reflect" -) - -type boundMethod struct { - Name string - fullName string - method reflect.Value - inputs []reflect.Type - returnTypes []reflect.Type - log *CustomLogger - hasErrorReturnType bool // Indicates if there is an error return type - isWailsInit bool -} - -// Creates a new bound method based on the given method + type -func newBoundMethod(name string, fullName string, method reflect.Value, objectType reflect.Type) (*boundMethod, error) { - result := &boundMethod{ - Name: name, - method: method, - fullName: fullName, - } - - // Setup logger - result.log = newCustomLogger(result.fullName) - - // Check if Parameters are valid - err := result.processParameters() - - // Are we a WailsInit method? - if result.Name == "WailsInit" { - err = result.processWailsInit() - } - - return result, err -} - -func (b *boundMethod) processParameters() error { - - // Param processing - methodType := b.method.Type() - - // Input parameters - inputParamCount := methodType.NumIn() - if inputParamCount > 0 { - b.inputs = make([]reflect.Type, inputParamCount) - // We start at 1 as the first param is the struct - for index := 0; index < inputParamCount; index++ { - param := methodType.In(index) - name := param.Name() - kind := param.Kind() - b.inputs[index] = param - typ := param - index := index - b.log.DebugFields("Input param", Fields{ - "index": index, - "name": name, - "kind": kind, - "typ": typ, - }) - } - } - - // Process return/output declarations - returnParamsCount := methodType.NumOut() - // Guard against bad number of return types - switch returnParamsCount { - case 0: - case 1: - // Check if it's an error type - param := methodType.Out(0) - paramName := param.Name() - if paramName == "error" { - b.hasErrorReturnType = true - } - // Save return type - b.returnTypes = append(b.returnTypes, param) - case 2: - // Check the second return type is an error - secondParam := methodType.Out(1) - secondParamName := secondParam.Name() - if secondParamName != "error" { - return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.Name, secondParamName) - } - - // Check the second return type is an error - firstParam := methodType.Out(0) - firstParamName := firstParam.Name() - if firstParamName == "error" { - return fmt.Errorf("first return type of method '%s' must not be an error", b.Name) - } - b.hasErrorReturnType = true - - // Save return types - b.returnTypes = append(b.returnTypes, firstParam) - b.returnTypes = append(b.returnTypes, secondParam) - - default: - return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.Name, returnParamsCount) - } - - return nil -} - -// call the method with the given data -func (b *boundMethod) call(data string) ([]reflect.Value, error) { - - // The data will be an array of values so we will decode the - // input data into - var jsArgs []interface{} - d := json.NewDecoder(bytes.NewBufferString(data)) - // d.UseNumber() - err := d.Decode(&jsArgs) - if err != nil { - return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error()) - } - - // Check correct number of inputs - if len(jsArgs) != len(b.inputs) { - return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs)) - } - - // Set up call - args := make([]reflect.Value, len(b.inputs)) - for index := 0; index < len(b.inputs); index++ { - - // Set the input values - value, err := b.setInputValue(index, b.inputs[index], jsArgs[index]) - if err != nil { - return nil, err - } - args[index] = value - } - b.log.Debugf("Unmarshalled Args: %+v\n", jsArgs) - b.log.Debugf("Converted Args: %+v\n", args) - results := b.method.Call(args) - - b.log.Debugf("results = %+v", results) - return results, nil -} - -// Attempts to set the method input for parameter with the given value -func (b *boundMethod) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) { - - // Catch type conversion panics thrown by convert - defer func() { - if r := recover(); r != nil { - // Modify error - fmt.Printf("Recovery message: %+v\n", r) - err = fmt.Errorf("%s for parameter %d of method %s", r.(string)[23:], index+1, b.fullName) - } - }() - - // Do the conversion - // Handle nil values - if val == nil { - switch typ.Kind() { - case reflect.Chan, - reflect.Func, - reflect.Interface, - reflect.Map, - reflect.Ptr, - reflect.Slice: - logger.Debug("Converting nil to type") - result = reflect.ValueOf(val).Convert(typ) - default: - logger.Debug("Cannot convert nil to type, returning error") - return reflect.Zero(typ), fmt.Errorf("Unable to use null value for parameter %d of method %s", index+1, b.fullName) - } - } else { - result = reflect.ValueOf(val).Convert(typ) - } - - return result, err -} - -func (b *boundMethod) processWailsInit() error { - // We must have only 1 input, it must be *wails.Runtime - if len(b.inputs) != 1 { - return fmt.Errorf("Invalid WailsInit() definition. Expected 1 input, but got %d", len(b.inputs)) - } - - // It must be *wails.Runtime - inputName := b.inputs[0].String() - b.log.Debugf("WailsInit input type: %s", inputName) - if inputName != "*wails.Runtime" { - return fmt.Errorf("Invalid WailsInit() definition. Expected input to be wails.Runtime, but got %s", inputName) - } - - // We must have only 1 output, it must be error - if len(b.returnTypes) != 1 { - return fmt.Errorf("Invalid WailsInit() definition. Expected 1 return type, but got %d", len(b.returnTypes)) - } - - // It must be *wails.Runtime - outputName := b.returnTypes[0].String() - b.log.Debugf("WailsInit output type: %s", outputName) - if outputName != "error" { - return fmt.Errorf("Invalid WailsInit() definition. Expected input to be error, but got %s", outputName) - } - - // We are indeed a wails Init method - b.isWailsInit = true - - return nil -} diff --git a/cmd/build.go b/cmd/build.go deleted file mode 100644 index 945fbfc78..000000000 --- a/cmd/build.go +++ /dev/null @@ -1,10 +0,0 @@ -package cmd - -const ( - // BuildModeProd indicates we are building for prod mode - BuildModeProd = "prod" - // BuildModeDebug indicates we are building for debug mode - BuildModeDebug = "debug" - // BuildModeBridge indicates we are building for bridge mode - BuildModeBridge = "bridge" -) diff --git a/cmd/cli.go b/cmd/cli.go deleted file mode 100644 index a63cb7083..000000000 --- a/cmd/cli.go +++ /dev/null @@ -1,273 +0,0 @@ -package cmd - -import ( - "flag" - "fmt" - "os" - "strings" -) - -// NewCli - Creates a new Cli application object -func NewCli(name, description string) *Cli { - result := &Cli{} - result.rootCommand = NewCommand(name, description, result, "") - result.log = NewLogger() - return result -} - -// Cli - The main application object -type Cli struct { - rootCommand *Command - defaultCommand *Command - preRunCommand func(*Cli) error - log *Logger -} - -// Version - Set the Application version string -func (c *Cli) Version(version string) { - c.rootCommand.AppVersion = version -} - -// PrintHelp - Prints the application's help -func (c *Cli) PrintHelp() { - c.rootCommand.PrintHelp() -} - -// Run - Runs the application with the given arguments -func (c *Cli) Run(args ...string) error { - if c.preRunCommand != nil { - err := c.preRunCommand(c) - if err != nil { - return err - } - } - if len(args) == 0 { - args = os.Args[1:] - } - return c.rootCommand.Run(args) -} - -// DefaultCommand - Sets the given command as the command to run when -// no other commands given -func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli { - c.defaultCommand = defaultCommand - return c -} - -// Command - Adds a command to the application -func (c *Cli) Command(name, description string) *Command { - return c.rootCommand.Command(name, description) -} - -// PreRun - Calls the given function before running the specific command -func (c *Cli) PreRun(callback func(*Cli) error) { - c.preRunCommand = callback -} - -// BoolFlag - Adds a boolean flag to the root command -func (c *Cli) BoolFlag(name, description string, variable *bool) *Command { - c.rootCommand.BoolFlag(name, description, variable) - return c.rootCommand -} - -// StringFlag - Adds a string flag to the root command -func (c *Cli) StringFlag(name, description string, variable *string) *Command { - c.rootCommand.StringFlag(name, description, variable) - return c.rootCommand -} - -// Action represents a function that gets calls when the command is called by -// the user -type Action func() error - -// Command represents a command that may be run by the user -type Command struct { - Name string - CommandPath string - Shortdescription string - Longdescription string - AppVersion string - SubCommands []*Command - SubCommandsMap map[string]*Command - longestSubcommand int - ActionCallback Action - App *Cli - Flags *flag.FlagSet - flagCount int - log *Logger - helpFlag bool -} - -// NewCommand creates a new Command -func NewCommand(name string, description string, app *Cli, parentCommandPath string) *Command { - result := &Command{ - Name: name, - Shortdescription: description, - SubCommandsMap: make(map[string]*Command), - App: app, - log: NewLogger(), - } - - // Set up command path - if parentCommandPath != "" { - result.CommandPath += parentCommandPath + " " - } - result.CommandPath += name - - // Set up flag set - result.Flags = flag.NewFlagSet(result.CommandPath, flag.ContinueOnError) - result.BoolFlag("help", "Get help on the '"+result.CommandPath+"' command.", &result.helpFlag) - - // result.Flags.Usage = result.PrintHelp - - return result -} - -// parseFlags parses the given flags -func (c *Command) parseFlags(args []string) error { - // Parse flags - tmp := os.Stderr - os.Stderr = nil - err := c.Flags.Parse(args) - os.Stderr = tmp - if err != nil { - fmt.Printf("Error: %s\n\n", err.Error()) - c.PrintHelp() - } - return err -} - -// Run - Runs the Command with the given arguments -func (c *Command) Run(args []string) error { - - // If we have arguments, process them - if len(args) > 0 { - // Check for subcommand - subcommand := c.SubCommandsMap[args[0]] - if subcommand != nil { - return subcommand.Run(args[1:]) - } - - // Parse flags - err := c.parseFlags(args) - if err != nil { - fmt.Printf("Error: %s\n\n", err.Error()) - c.PrintHelp() - return err - } - - // Help takes precedence - if c.helpFlag { - c.PrintHelp() - return nil - } - } - - // Do we have an action? - if c.ActionCallback != nil { - return c.ActionCallback() - } - - // If we haven't specified a subcommand - // check for an app level default command - if c.App.defaultCommand != nil { - // Prevent recursion! - if c.App.defaultCommand != c { - // only run default command if no args passed - if len(args) == 0 { - return c.App.defaultCommand.Run(args) - } - } - } - - // Nothing left we can do - c.PrintHelp() - - return nil -} - -// Action - Define an action from this command -func (c *Command) Action(callback Action) *Command { - c.ActionCallback = callback - return c -} - -// PrintHelp - Output the help text for this command -func (c *Command) PrintHelp() { - c.log.PrintBanner() - versionString := c.AppVersion - if versionString != "" { - versionString = " " + versionString - } - commandTitle := c.CommandPath - if c.Shortdescription != "" { - commandTitle += " - " + c.Shortdescription - } - // Ignore root command - if c.CommandPath != c.Name { - c.log.Yellow(commandTitle) - } - if c.Longdescription != "" { - fmt.Println() - fmt.Println(c.Longdescription + "\n") - } - if len(c.SubCommands) > 0 { - c.log.White("Available commands:") - fmt.Println("") - for _, subcommand := range c.SubCommands { - spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.Name)) - isDefault := "" - if subcommand.isDefaultCommand() { - isDefault = "[default]" - } - fmt.Printf(" %s%s%s %s\n", subcommand.Name, spacer, subcommand.Shortdescription, isDefault) - } - fmt.Println("") - } - if c.flagCount > 0 { - c.log.White("Flags:") - fmt.Println() - c.Flags.SetOutput(os.Stdout) - c.Flags.PrintDefaults() - c.Flags.SetOutput(os.Stderr) - - } - fmt.Println() -} - -// isDefaultCommand returns true if called on the default command -func (c *Command) isDefaultCommand() bool { - return c.App.defaultCommand == c -} - -// Command - Defines a subcommand -func (c *Command) Command(name, description string) *Command { - result := NewCommand(name, description, c.App, c.CommandPath) - result.log = c.log - c.SubCommands = append(c.SubCommands, result) - c.SubCommandsMap[name] = result - if len(name) > c.longestSubcommand { - c.longestSubcommand = len(name) - } - return result -} - -// BoolFlag - Adds a boolean flag to the command -func (c *Command) BoolFlag(name, description string, variable *bool) *Command { - c.Flags.BoolVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// StringFlag - Adds a string flag to the command -func (c *Command) StringFlag(name, description string, variable *string) *Command { - c.Flags.StringVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// LongDescription - Sets the long description for the command -func (c *Command) LongDescription(Longdescription string) *Command { - c.Longdescription = Longdescription - return c -} diff --git a/cmd/cmd-mewn.go b/cmd/cmd-mewn.go deleted file mode 100644 index 95bbce925..000000000 --- a/cmd/cmd-mewn.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd -// Autogenerated by Mewn - Do not alter - -import "github.com/leaanthony/mewn" -func init() { - mewn.AddAsset("./templates","vuebasic/.gitignore","1f8b08000000000000ff648d418ec3300845f79cc252769686b9c4cc6e763940e4093472e440048eabdebe7293aeba01dee34b1f7fc669ac6a0ca2c4d3a6741476f8a6ec15600845e754024b0bb7dc1fc8d2f025cf335e0043f8d3e50ac9be7d11ff1f0b165d223c92c907b399dac930845fca552d50369eab5a660f49e8dd99891360f3598921a21f0a11a5368f7dafbe9baedd17e9f31ee1190000ffff7baac6f6d6000000") - mewn.AddAsset("./templates","vuebasic/frontend/README.md","1f8b08000000000000ff748e314fc3400c85f7fb15966e81816467cdca80ca8890925e9cd6d5c53e9ded08f8f52864483374f4d37bdfe7088b239c07a514428cf05ee586c940d1bc84d0f77de03203b1da90f3ff1d628cd0c95c28a3c2c0235cc55e2a66194685492a8cb860963223db8ea8cea058177c0099896922dc08a5cae8c948f808383be571079c9ce147bc82a19a1eab6bb437df886df34cf4bd4a56ef719089ef069dabc94cbf084978a28bd7617be703113ebbfb0c4e3861454ef8f574352bfadab62953b338deb4917a693744fbdc84bf000000ffffc58c3e8870010000") - mewn.AddAsset("./templates","vuebasic/frontend/babel.config.js","1f8b08000000000000ffcacd4f29cd49d54bad28c82f2a2956b055a8e6525028284a2d4e2d29b65288e6525050505077282b4dd54f2c2850e7525088e5aae502040000ffff3d000ef435000000") - mewn.AddAsset("./templates","vuebasic/frontend/package-lock.json","1f8b08000000000000ffecfd5793ea4a93300adfbfbf62c7bed5b0e4119af8e68b834038191032202e2642a66440de8b1333bffd44d30e68e856f7ea5ecf3e719e9bb51a55a952959995a6322bebfffe3f7ffdf5776484e0effffcebefb0ed5525e81949d243d0bfffe3a1a90259eec7d1432bf20bfd853c3e0d62ebe0f801d05e5ad1d3f30ca4a59f81fceffffcabc84a707a6683044436882cfff4fc01e05f7ffdfd7f99860902d88a6dd073b247f88f4d9740a95fc813d0534b06f238a880fdd0e4154592ff270c67c0f5f3226b7f4549b8cf7fc5990bbf191dee9dfde89d06fd55b8c7d781fda8006ee617edc3c8b9679028d65b3a23ac04a17188b6d980326d5e3d6c62978af432f0057abe2767aa65ec73cd12468994597c58346b539b28f334b4e4830cf4353361d64d8e4d7c359d2cc6bced0373f85ffff50ad506d519aa9e66f882c2677cbca2cbf35d2ff05daf78f8caff7ec4cd53a7ff39fdff3fff7185deec3e62b15fd8d7119b3da2347b4026f60bfb1899245d31c87a5320b435b40a929d03720089c7e98a5026f218678e308cc12cdfe23ba0006ed2d794313adc06b301339d9aa32db22a6373c7e4f3e3929a0fe7f46ad6a70057ff0e322f78ef199bfff1a69b0b229019459c3df53a47db19654090802c7fe9736ba4c4c872f0fe30050893c028c0fb9d32e381901f746a1390dfee61c55105b2a297c76566815e6824a77ee8cbfa7e41a859baa726e2ba699fc711796ac2ae9b82d83672eff935ea177ad1fac4674f00f1cb0fcb41583d2188fc45fc422fda2e3f16f9459ef1fe190fdc10366fdf7f7d7e2de2c85fd419d4ee0be37578b877f6a3771af16275dc5c2168cf779736bf1e7a888f80586d87d83299fa7dbe5a0deaffba7cf795cf5f9efecf7b42e09c816f4b02fceb92e06570b8f7fa77ef34e4c732c1c1a551b94a926d53ec966c484e37abcc804b5ddd87a2149474620ec783314cef8f8048147d80674ac1a0beab30725d1d80188c48778d11c7c9e47084b0cd8c9f485e2afd8e4c385f3557ccb9cf416e3db13c79c99befb3fc4dce3d6b2f323fec652f521dfd85fc42ffcdd95d38fb51e8f68c288a0ba3003d23ef25e53b1aef374c893ba0e0debd96ae4606aeead642ddd3874ae64dde54e78ea4a447ab25b52d355defe0d1689c454b631767938980a0c63641a1e9a22e27538d10c7e3782843e1526d67d6b2af8ee1b5932e0f39fe6d6be02303e369f266e907f6c3ff7e64646dcfc873df8d421015bd38791209959ffbefc920f4b769d3fd235ea9d6fd9ddee9133fa6672aca6b629f858b81806a824017be59b5ebe16c2be5d82a340eb934a12c74345d887ee2007c3c48a0da156b378fe11536c4322cb45d6633dc067b71b346a4f56e286c06bf65e73c4d153449f060f13cced13003f0f02803f9330dfe9bba56e55f6708cb08829e0d02e03e1a333f45f30b38af64bd78dc95723a5be8bbacc6a72ad707a134e30a2f28387157e747c69a71e440772d3f875c4dcd26043c09152d2347a354126527ac5cac85f4313b145b3d36f8232ccff2a8cc47f56fadc4a7d978b19f17bdcac8fc07aae5ef5aab57b6e1f71134030fa8b40223cf7b0e308a3203792f094ad78f7ec2aee800f68cdceff4ea6a8b14f6660069030b78982a13d35cb7454d8c632f254bad5e3af4a17fc8f6f58660d1602e5a33b728a28d25a6ae1d833c8f50cc1eb603e9608263604a1e166de7b2a843bfe7ec3d4ece2923abf0e3a817bd7a2ab7c9faf4420842f371bd3f2dee5e11bf8cf23ef73c8d1027851ffa39785c476fa4c4bbef3e61bd2cfca013ac0c248161815e5e5ef84f7817a6b481e347d796cf778b985720af0cf7faacab7069572b6b33b02ddcddf122a4a2a1a76b342aa97844968ad95063d5760662b432f732b2166aa69cf9f4b6c004d54d92503c52a345a3309b7c6a1d159f4dd091bc54566bf78fb2d7b5d478df00ee40be8f34d24f51f45db8af447eb75b57ba8b6b297166d9020df3d1ace0f6e631a7754b0817bba5db0f25219cae19c8338d90b5a7e44a4c0c39a4498c22b5318d8dab0557b6019263f9c41f87c20e62a3a82a9df4b7c4ca4f69886b46fa29ea5dc079a5d6c5e3aed419d2e4968d4789892b4b88db1d17840c93e586744480f5c77b669a3a684c9756cbb7a3d047ed6d0a33b835f08d8de230075cab40a13601da0783629ed96401962367fa1dc69a0b8ad71919cf5f7d5feb5f6e1b7d1b4d6f7ec64ff9566f81bd52f76d5b57ff2ac3c6e602b8445ee95581cf963245a804d870b93a14d63304dda1768e1e9b01272aab2251787231cced493560c4769968a93df16687c37a3c77498618e64d78d80f83f04ffb576f2dc09f22c215a4570a5c357445ffd4adc83cb0b72a90b9bc3cf207db2e5aaf9d014c37a2226f369ce8534388e2c118c5d8663dde28eb03462f46dbc498ad86a660cc573bd90fa8080a526ab0d1314da77e6b857d01fd1f5a543f458df701bf12e7fd7e5d696554311484b014ee820aa376b98f346c9c610e67a59b29d91f9306a0ede32148e6864e5992c091e240931343d4562bc3f5d3b0a5f88cdd73e87c634f8565ba2a492bff2d1be52bb48aed32003d3f4ce2acf851da5c003aa3c5c5f3ceb85fc15ec0a7083ade88ac3f76894584e13e34da361b38ae0562938cf9896a26fb0610a28c331eafed24d2a758649735a504573538cc81e5b746e832a83ad79b46e8ff9e9df065dc179911e54e9c85f7d1ff1bb1a77bb0de50e0b5a96b8c4a5f8fa9797f9357d07c562aabc3301ccac4ccd311fbb08a4d97778a598258ebe94eb6b37515eef44136a90c004fb51237c70892e6565971f866356370dd161366aba6d977f8806f98fa43b72af7c324003dc3b2409e77f11af324f08b0709126745cf0656606446f191cfd72d5c753f12f535bfe11df7f4a716fb3d90af4c77af475701500e229b1625b0d245491bf8ab4d09273caf33e98e31889da1a3fa663d11ca26f36d9aa8b9392a89459f12d91d351d0292b36084e765cbf443846c7693cd6168677bfa4f0bdf2be7ffa7a8710ee69502e74fbb627da40f97ea68b8aed92662fd86b7997e3e0e317bbe8075971d72630115964000bb92e66dd81cbb5a3c30fc4db6ad2d9d5a321e84b20656aa96b6b61aee804c17d58692eeacf877f0960117343f88b0d3f8af983afdec8a22658dc022228e47963a575933d5faf66809f1491186b23491a8944202c76219425de7fb31c2a2b3a36f65b53a838ca4cf0c256cef8db6b444a8e9ae256a3f28f79be0f035c6fc9ab8c84068243d236f23ebc12cfa381cfafb3eea3d90e724b8dda3abe78a3b4b0ee1a1891db80399348241097b1bb65f79a5339523c41411ba09317e2bb430bc668793c1613ccaf478c9707a381c84a4188750aed17ccb8fea6cd246c3019c07dfb19f742318f7a1b2aa3323793157dff7613b39ba3fb4c3f166abf29e6d83ff36fb9c033a679af3e727ab06efe082b53e981fa3a9eaacb6ab4d89b4bcdc57495238f6a9e16e4c5bea01611c3f5b6e8255b60d18cb88e8e911aa66d32364ad9c32aec14e8e56c7d53cec535a2109c872b3fbcd34a67fe946f59bf419fc7bd8e3dae4fa29e17201e795392e1e771523da01a2069eb85f9bb95f12f471b81af28d62420b551a1d51230e127b10e7cb09da67a6115fe4608ca1913973f9d554adb32d5b663bd71f2dd5b0ca71b6514a624236fddf73cf7f682beb1dfbf6a7b4ee3d906734bbd3a3ab6e16b687e522ad8c3e663b23a41e9021ebc07c69ad56328c42081185e15a58b18cca4090e7fb8a2761c755b185437623e161711cb1fbf9a25ae983a8ac9b6d2101a9568d3f6d345e4bff7b52f577c97101e79506178f4f32b503e263da59a1cc8ee7e54c0f1a9dd5db00336468129411e9167b653e1df841046d4bf6282939b2e20d773895b7aa9566e5a21e938e400cf6c739ebba232e993b1bc55a53c977c48a3f11cef911558a75a2f97df1889f65567d89ccaf12313f857ed18f89290db035bee7722d51359a0edd2d89b8cbd568535b31cd5ae43c88e081d696449c8bf3a5e4ca994daf279904d42aac6c7b22c67c2db44b75a342a475f063964bc6f1f4f7f65eae69d3210d14fd457e401bfc23da9c65177fbf4c7c1efc813ecf7f779573ea4418113b3019110325a9f6d468a0ba7c551c0cb58c2b690b918db8c9f2792c4c07317684b6136ecf1922ed6ba13c20ec55ad8908b221ea5598567abc9be0ec74983b5f535396670487a734c02bfb02e4af51ef87c6abf4c15e111fc0935d437c281c5f9285bf3db5e27164b8f7f447d70409e928aa236cbd445dc3762127c750c747d412599653f1b8e6d8d01d35312f1ecd911d27195fef9d70571fd52c514567b132b475cd24639da08628d5eaec74d3ae4110dd11793711f2e8de27599cc4b9113cf94eaf8ed3c7fbf1bfa1423a037f406bd7be5d950d347662581e49695637c42029786dcadb2c4eebc5665d7216acd37372d2222b743c66183ac887b3646fb10ba98088b9ef94f2525678865b2ccd452a54c6124de6ec80ff0e65f3f95c8fbb9ef87dedf204246fa3c268ae91d955ed5cd3e43155e8e127c80aff9d881afe7d3c730df306ab5c7739adcd0e1c528bb3863fb09c2261468b14516e84d89172a63bd58710f2e88f174ec016f870a7acf078cb73b34542cf52cfc96206ae6ca12af0dc36686768a6d4249f0ea4d538db31dfb11bf041dad8b356bacb317738ec33a4b68115bff0ca8f13f915da0df2be3676252cbe81e3911ae64b029f84bb790ab2304cfb0a67ac65dd0b3db86acd95a34d6a6963cbe6a0a90f3e3e2887f3ea108dd6c7b29dad4bdd19e9e1e2c8d3718656c8d07156fb7f30613f5afe17c4fccac2dfe771d4cb8bcc8fdc3fa228cee1dde088f3e6aeea40184e34743484752dd4ebdd147118a9dd7ae11e6166238604ee6e36e236f38956c1db513304da743e73e21c97ebb17520f86a1404ac381fba542370cb29799821f6b155bf8327be48d32b927c85aab1b90756d1cb405ef4f2240386fd2386d387506f50f86da7ae06d7780f04195a6050900cc583cef34b28ec4ff7b122b4aeee065cb8067d6bac98930ae769bf1f863e3b70409f1908c548e67789b6cdad317988a8bc2fbbc52c1bc7cc26fe8e7dbb2fd2f92691be44ede4c17c7a50974661793dd38f6c3f72ffc46abe0df916d56f76ecbac24357dfe3fb91d5946e9f53ad2db164e2e5e2888f84ac5e3b12cbaf2492ec23130ad12595d931546958b4931a1b23e4d6a8bf518f539fc30aa4758f7b45b6769176a4c87fe10abf4bb0af50bf8cfcd3c9cd272ba9fd2078f68dd4bf0df906f56f77ec4a7dbe5a6bba491dfc43b92ce6b10a36ca61d948ac861ef48a2799be8afb1b763a3aaec47599a3c271518349ba5c2b7b08d91f9af87850645d5cc4c7d5d2b69b30b5d37c9d7c4752e2e7cdfd47c2dce8786a4acaded301e50787b8231fdcf7037e8afe7720bed2fd4e87aef44677d97ccd5458814ce5c0aab558ea6baecc27bceb8bf04493d463411fa326a62a6c5911fb45960fd6faa0a890ba1a8713114fe7b6c06db2501036fd76874c515a70a63fb8dabb13a983e9fd6de4b96578bf69ea4a127c20d9a9166fed992a394ab28bf14caa53c9de14a3882c848a28b57e86ad05474945a6ac881de31d295a760c69c3295ad8ccf66052c1e368ab8d60c87067a343a06f7e50f57e82246d6484bef5944af407c87201ef2d692e9a3beb47ad2917889ec573d8830b73a2aca6bbf5c0aafa860fc98ac0894c8a38036fd2e4cd661fd0449a7ab909d55b331185f14ac52d678d2af35c9b1c553cdab63b216d39e207256477f2fc21f7e406b437a4f98a6b42aa533d12647ecda2769aeeead285507e2e274302c167c7407672b7bfa2b5adda5798bd25cf8018d4c4b8193714ef2613c8e776f1720a43e5318dd733c5b6d4c4df89ff0851b6cf7fd2c4780572830c9d0d07ad150969282e0eeb4d3f1833212b1ed7f9b1da39384c0cb7c69ed6d7d4b4ce36008874c132ab726b86635d5369536744644c21c7d146abd516996d528c24716d64f667ff8865f109ffeedb88f19e7777b74b575215c8829bbae901db00880e8d162f90666c871130b661dba448e303dde266cd3ccf16cb3e11a14b5fd8886a6191aec4a1ab212226f6689d15bb84384273d5e04a3793ff110ae68f3b67efc27d4bb2df72cccc31201a4ef44c849f53f36a37f387287e701c84631d63ba8567151108431aa214d6eae39e20560e17f7779364eb71259b40f3399ce2e4d422c45800e35d6ba8e69efed7aeb0972ce99e916571fd47c2287761be92eb6e97aea462d71435321a141ac083fd88a19d98506db48f2e77c78d48a646ed13d3321f976bde1a9be32964cea7ba8e214713b39de1c456d6d363e6a458b35b551364dd27d859e487ff5a55748693eec98cdf4aad5b098d1d7a75a5d9889d1d5dc225d5cc5883f838a7c78463068c3e64726a2ef455900db529c52eb44ce147e401780862b025af2ad09240a07031628fc6f6a03bca48e5e62d183bc27232f98e40d7e7f3ef7f2034f6290e3183d83af4722b4e80fd87d7f46dd0b738e576cfaedc1215d26add4f5158e4cac40b5a8286245ff2151159f6f17259efed71dff6e67b79c3ccc926c84c75e25118a2cc9323e32551c63b1556883398d956fcdc30518819ce917f8a307e45cdcf6acf3b10dfa7d627346635565c07a577acd60f1b9c6cfdd502108e8c60e14a52558b11eb8d24ef265bf9a8c866be9848d5aca0c6692be33c3788979b213f59e60b68609a29e31efc6c578031f96762d79fcc367f83af536cec9dd0f26f9c81ba07eb16d99e9aba9e8172d95de52a2daa15d6786948458b0628a2e07b295c7081a835fc7894437836d256593f120ecb31cff28de6a7e4960ea0cc694c1fe0dbccc98491aeeb7db0c58da5b5fc0e827d25b1fca240c48747a03e5d69e35f5e27e31b4f74b9416c1a4f9f82a29f5738561c266501ec2e4915df29b66ec0bdb90ade76eb2ac20e2bd8ce52a5d1fbdb0217c584f3853874dc8318113105555cc361e3316731533a9fed49b49aa502336d07b53996e10511084d94e62889c30a0e8dc3113b3c8c482bf9d7fa6aaf78b1415e64a55594d97b7ae6f783a67720de22d54587aec1523edbc32589ab4eda04703e6dda7dbec0b6622196ec7edcee93d4f604d1238d1db49d2c6dc5e3893ec7ec314a0521e1ba3138ca3ad3709ba11ff467e56056d429c8f57f8a3160c7c58380f9e940d76d80374974d6de7519e55c13b533c73fd64071b99d436df36d098fc432e1bca3bf4f0a471120333ec6d1bc6cb58cdf686aa5ef59ccf44a9911f8812601a1490921ece3129d5656a55a96ffafc962fb4c580bfda094d15bdc9649e05b0f8aee00da3f23432f41de24f8458fae244fa1b66c36c43857f6a205545f3972080f0d1d09398ee96c6328bcafa6b3e12041dca6a1821265db0044ce710fe61bf1b06afb0046b429bcd9d774e9d07c09a49a5ffe5316e583628d4054f88f27369eeb05fe1182dd817d8b7277ba762561197a84b7ee8b5458126c90d2d3e9405996b48098c6a10af235cb8e20df083249ec0f199988c7529f4e1715aa156bbc6c024e1849c2a83a1ea82a43a9f58cb3f64d7ff01dcaef5325273fb495be83219c38ebc5ce1fa1ff23a85be47e6ce94a5dee48090522e71a564887fe7e4a9a4c45269e661e6cc4b5314a76f531e1cd508317166bcb1310fb3833b655b6f1b41db46986c1381e723b4a6df1bda66cb090db4ae8e60765f2e7e8d1a920d5b792e5aa34d5fb1d3adb9f1bf790c7351dccec8a19613b226c94bcb228ddd6a91ad6fa0c56b5b4a5cc5705cfb2b4e8d5f1c6af8cc6160558c6291094a49906fc0a4e973a8df4f9f2b0a483c39f3d6bf48d540dfc0264c63be505be93a0cfc06ed1f2b9ad2b1931c51b7b2111cc8958a31c4dea27a2c841790359f0542061d91078a4863362c750f930026338f1c78ba2b6b940b40e5bd04e39aa0e6a1763b11ac747f0cc83b4c9e89fb221fdb8419af78cf027237337e1dda2cd5973e79c020e1b0e4a2b3da2a997d9e95ea635613e76e655a5608597b1f95c3a9a86625ba434397ae3bd5461a3c362413a7d663b3fb86668c446831f990c5bb795060f8e205677df61abdc2aaaf34756da3312ad380ce368ff6756dc35d0f7a8fbdca7737cbcdf22a5a14a99b4daaa59b8873c373ae483cac078e468edda10a894526c58d75ef3e2c10286c36d59bbac28e9e84a43960814282feb389ed2f688e2279ebd1e9ada378617be85c4f7b783ee5404fa124fe46d5e80f00ff3c433d0f778e2b94f579e30f4459d0cbd98a30d0885967d8cdcccf795a0a3687d60e0f8806c4681568720c6c3d1de12d7fb028207ee6c9331a40f2148a8ee4bc4dde4cc418a56895d49d46a1efe5e79c02f147dfe81455ffe61415ebe2fc8cbee829cd170b3ee0bad3a43fd793ef5b6c192ebe75b2f948d6cb72f167025f863da16c3441f48dbc94452f650bfa2b1c4725ad44f8dc1180c9dc5a48e1b2b936145ddd9463fff7fb5207f30cfec9e65244f5b6a6e169749fec176d2ef9f89eaf601b7a8fe5effae27a7c4661e8b9a77a0778d0cd1811c0f259817100d53d96ab8e68b829da9d97a3ce526c6b059ea92951d4438e2d72cc498a66f8d86d56e1550b5b042b67d6496cf58725c485f6383c75da15e9181472b19f9bcdc8d40dd2b8ccc053f7236fb1d7037c9f3d2daf5fc76eb47489f9ee87bc383783375a640718e736639060d0c2048770c249b214ee218215d2a66c90276d990537e9a6128e2ba4b8da2426d2dc62095f51494462a1f35f99fb211f49405760accfc1199790ef0167dcedb3bdb4642ebadf4dd5c9eec86e970a51d7cdda46c1560c35cf331b81e292454efec5c5ce2e824de4b0b838e7202f18ebbbe4a3b24b20852a2eea7f5d16618acacd31d8200f38f1e39f9284ef6299a26466684a078bfccd3f751f415dc2d7abeb676de326068c8f354df9ebb6acc4888903702a8bd6336f0fb48241818e78205276df42c9592d462f4224ad6d0c23d18f64edbc506bb830157d2a1154c0cadf6d6092fcac7efd8b5bbbe17e443fdf7c942d8dfb8ac1fa4f647c955df2973cfe0dd6282b3e6ae5237df63e931679b0137d62a542a17166cb38e1f1c5c7cbd5e552bbd81b588e38b79a149eb4d816e528704da68c9ef428c1ef9d389a3ebf910d7567bc73f2a230d919171f07515f8fcf52f737a5686f8674324591915fe1fdad37b827593248f4d5d17e57eee1e82fdd84e89b53e8e16ad94fb1bd3aea600f830222c95429e714b38734c1b0eb74c23267650064243d03c329388a47451676b445113a7a339e4e8720068f02d4af027b2db2eeff91b5cdde5777ecf1f7976db5927eae75e9c159e11fdf154835b806ff1c5ad7e5d99445a11401506b867d0c7502f12336af70a3f9c7252e6174372bb81fd296ded8b72391fe4e8bae4615ceb1bf818c833872b8b850419adea80dd626545239d958ec64a4aff29bb841fa6ee7f5f96c175c6fe754bd774286ee31c5a981c54a5591732e2f1291b659bc53e17a6c612c84c4423ec312217c0a577038e9b852c6faed076da06421092625f597140a78718a41ab2cedb8de1b908fa4fb15cf3c2b70e3f7f80f636c09bb4396bef1cc3e2f4513c35d64307b20eb381cbf2f862a61a13ad15671cc087b298efa0617028dc19a8aa66eabbf1bc283276610e7c4e447089628d6350a179df6a3006b3247c6198ff84e3b29f22e573c5b03f1b1f7903f51651df74ea4ad9c961e53708b2a087f076b3ad082de6168c00e443abd30daca4738fead747cb7602925a2f16963318014f420665ed78a3b158ac25bd98491ca3d4dc6eccb67b16c5f9fa5f55e3f63bd66bd12620767a791b9a71f067087c0ef12671cf3b740e85f1a2c72a1bbdf1f536678a8c51527e00f106b29fc79214cd654b5d5814b1d42b75ba0e0ff941d409f6c88fa2fd8c39daa1890feab4500cbf496254b40ad6aa84f1b7c45abe8348cf87ffff9c54bd80788b48171d3a07c488818e048297995b761169aaa1072d46efd75b53c20f79b3ca645471070524ccd2e3d62b187d58cd4914944bc68887c18e5fcd721fe8f476a509ce0031c806b156d61f2d3ff233f95a19c841d10351f513352c5f477fa0e3cb8fae952c27332e1bf7c78d832360d0b83369891c17bb445d39dace85b6b57b24e1b52d6f46e6d1a0255e3c109299108855ace374dc8401be1cac8523838e559294b7603cedafd2d93ffc00cd53c74e7501dfb99bfd6a943b45893e7ef16e951bfcd207baf7fa47a5523e1ee26e81940f87f8b8c6de47af7e166d5d4a037df8f29751f6ded9c7cfbc7cefd456c7f7ef9ed5fad2189f9efbeb3995cfbc7433ddff13035c279e7fe6d5cb74e8cfbc799d5bfb8977efe77b7e6290971cc1cfbc7323cdaae3ab67cec027deba4ce6f9c28b6719235f78fb2cb7e00b6f979ffde80fa2a3f72b19be1fa4fb5061dd0d207de2db2fc2149f78ef726ffb139f7a67efed13a0bb8bf7bbfb149f78f3965bfc99d7af1cae4fbc7aed06dc78d5cce23a07591ef879f1647de2bf88f31e7e744a00891e9bb1ebebb3f6792f00158872af004f6537d193017b6faff5a30ae01f6dacff8665fbba8ffeba6ddec9a6a5f6d3cd204934c4888a3e4e25f374684d1c693c9ea171c6aea60b73606c1d353e96231597d22d956d89f170e0d799b9cad6a867cd9089a2cc8e04d54ad1013203b299ddbba2e913518c57549d62181f552f7b9ef483a7b1cfb11fc4f03388334c3f3de98a713d59a23845f53d1cf437edc00eb0c540a1a53aa8e224834ca2c846ac3763a1161cfaed3cd50679d377712e3eda33c1dc0226d9c7b9b9f2a1b94feea82d1d6f273395fb1ac61fbebcf7a40ab05fe42feada7beb408e33a809886c1059fe7d30af0f2f2983fdea5f2ccdeed4791a1bee3dffd53b8d754186bba440c85422bdadec59e9f4b0da255b76c2cf93c66877b9a58db14329303b5037fe7cb5620db73c2c89307557437ab4f1d46ac22c9c165f8e6d0e4be8a69d79d4445169983a27c515395e9efecf7bcc7c763dc1b7efe43f8f0df75efeecba577f5cf3c85c10106ca88e6bd773485e90e5317ba47266b4c4f4480c13172f37ca6e0f5768664d312694d4ca98aa83a9a70e98ca113cf48071f3a1bea3163ecd6d57aa4bfdd67ee1490338d9abdd7647cd3ed7faffbbf3258aef92e7f562886fbfa1ea79ec07f23cfdd9f51eaa1d8ec6ea18761615bbd1d64820ee0ac7adb48244b1499c8d1491d3ad05333557262aed9c0a109be96cb0c95b0a124085e330413ade0af66aa9a8dc32176d6b24b8bf17ffec489eb76edd2d0a7df550c4972edebce2988f6eb0bafa621b98a5fbb2dff5d131f0b3d64f964478fe846f3f547c1af881051ffeef7a4418d7fbb34113a841624ed7d0b6326a7f86a85be0a5adad8462c826f171636ddb30adadacd68701c9091557489032e93b7d161968fd45995115dc1eecb13c81366376befddae6d8bb3785dc40f2596b11f71c232faeed6fec9d8dea076464551cb806fc58b1e0087a569e474614bf566cbf4523f417f2abff791a7d0c0feedd6be99d607690f5629f562239432a7f2dab7d8b1dcfad7265adfa94642552bf9c0d472e3998f822bd1a676a5fe6dbe90617d644e1cf65536f5af430de0cb5553d86b6b5a46c58d2f2dde1176d94c7093c11ec6abdbeccee712fd7068e5106c5edbe499c1756fe71e021ccc220b2e0073fcaf6b31e883c23b2c0bdb8fa83eafc824d7907c8835579f5a87782d0a178da6a96f417fec024d0fca82c2d63d5c7b52032cd2529b67dbb1e0e9542ef2f94bd478ad892cf4c523ac08a475aebc35af20e9b5101b18b5ccf4406117c1e9a26511fe05f53cfa734b410f442a3359f735490cbfdd90741d82be247ff2d79b22beffb4f516c83c0376127ff9517c6bd2cdd4befac332dae06877bcf7ff54e0376c804f386e1b835a41131a3b1e16a12cf0d6d3c1b35a42917f3d8722bb396da66b5ce4c55c7100d6575c594c796b731cb3a10a6b886525692edbc611fa2060b8781f0b1b2baa37a2ff192c7b6013b990f223b687b20cbe22cefd5c04c0cebf091f0a1bec2b71d003ee0efdd0ebd13ec8fd16a6db86c6a4cb650e69842366cb48aec8f8fde4a4c978bd5ae14e732369b0a2c5fb8d3a30955b9706c7d970ec3807477b09761ccb490790db25718afb2e5901f1853ec8b87475eafa57ab303709a622f2f4ef37bb51bde5c5ff5b86ddfab7dbbf06ee8940e6e9411e5fe9bb8e75b79845eacb4eeb47d1d1eee9dfde89d46fcd89f427b3525f40b406063dd74497f5fafe32ca25a67a389830e1ed17f5c4d342fda00bcef3362bf33d3c7f19fa7faf8eb86acbd33d762ccdb02ea63a240a304588712e61e1865c5e026f1b9b93e73d6bd595ef3db273ce387911ffce287ff6f88b23b334b9989a501391a56a43891527a39e3465c442ce0857b7766579f776305dda0ebe38edb25054fc69b6524e029ca75ae1fd02b4beed4db33f2dec3b037d7dc5fcfeb2e79ed83dfec5326a7c869cf8a83387bbb3aff3a73d92fc97731fa3d1ae257daaf3b0d5f87877b673f7aa711bb50d3d87b4e3957e940469dc10459fa29a42e087cbab2eeafc9cf50f375fff53d84dd56ea8f6e467a5741915f51504f83c23d38ed9d86f858cf80f438b060e4508b3ebc64679253cdcb85c62b7981fb1102a852e140a9431b1e3e3a5c84345ab95f2a098199325ca96a7cf03872853149c2dbf4622b2c8599e59afb4eb7d45725804f2e57efc95bad4a702a491d82cc7d0c31df73f01e5603d23341617cc5cfeb0218ee7dd8a577fe151d763ef73426a1536bbadf5b05b0182762422de5e440b0db9da1a528e2053b8698c9363a5eb825616ddc849d870bf9682dd6ea68171a795ff5dd9aa51c62be5f94520e2daa4e771cbecef74d24e1695a3f8ce67b605f907cafc32751ec54c3365cef86ab995fc4e19a85f6733fd9acf788ac6c455948c152b69793929e38fbd4a537d36523dba3fd72dd804630226787b5a434ae28f738471bc74db29cc3a32f1e4af8728aca75cec3bda054c7d5f3a839de10f141791461d02b0c37bf6db13dee14fc3a00d3302d237f4d61baccb6afdcd731d077fcca336678f4568d24b9c375f82fe22b096cb720bc32d8cba3de69f88f796945cd8de5a410549bf47d0b5bcd605fefeb2bb75845ad4dc9c721341c552836c027e4dae2cb4251d75e80e5e108b536ea6232d532d553c25c672d532620a45f736db8f92d5ebacec5b971cf64f74ca63797dd75b920f3cd051f9f67e38eb1e0f390cbdd97ceb3e5ee77eb32d4db08dafb4bee09f4f3b4ee2db10be176751dc983dffd62075e6eeb5cc4a5fa673983ff8e367d5fb4e90e317f540dbe00ba964b9f5775d12e1e72cc1af6fafe4a90fa7d1f48d6dc0e921513a65ea1aa96bb67649240f773ad916317cdea636621c83ec84b60f171745485cd4a455686d9fa453ea588ca13bf7a76e4b75551370ba5d3fb79e91aaf8112237878ff73effad12963c6fbdc5bd5838e07c1675f8aa3b76f7cc4af77a6f8939c7b0be40b0fdf6afc24379369e5135ec836eebec858797c402a27e0fbb43d6f0ee361795858fa8c8e897e1f6707a287628bc40c3368610c36a17b18ae0a45f30e34a26a96a64e46a31d3f6342eff7ca90bd67867523d0191ffd3c699e815d11e5f9f127c9914eb7bb7ecb4268911d065833d216f41ecfbddc9d436b0cacf638140f4c8c05d44234d6a954098e52276e4332ebe040a4283452f6950e1a3b656428253897cae5c6faad60f7ef93e37581fe3c359e605d11e3e9e92769d1c755199f0b6c315416c723870500160d12970f9899e0fccca97703204c6a53318e7d80a540a39d8c09f09d6e78469f703d85988f65331b6fd530441608cd0e8f13e7b78e4afc310fe577d58265842078f15dc86bdbef0307e81dd7a68b1d7606fb9e2576fd459fb0c49e477fb0c59effee916f8eeedfe52ac748eb5d1a6d1ab389a0096ae334ddbac00825b1c5835502cfe8593f13b354f78889ac8d0a4b8770ab3451a1190e623a1cdb24190a83a1eb95257c285b7dd8d794fc5bacb10bcdf827966a1cbd59a771f4c945ba9dc1217e405196b3051d1132c525204faa06cc4418eccbe34ca149cbd30f6e330eabb1ad2d2279543a34145a6dce2e8a623326565aa94e458a9dc5f34d3ef74688bdfe0e67f1138bf4db96d8bf57c94fae122bf07b7105b2c068bf7fffe46cf0079cbdfeeaba6b52f28eb4ab84913774705512d7d0461c1854355ca5955163c52247954c6c562e34ce2833a4669bd82faa4906c20952f19b43335f0b2480b485ab6b783d3bd06cbdf18e772c87dbb87962e3134fff0c82ce213c61e9fc5157540d347f795c18033555a350085002c38a013b0f46aa45ae802b0bb9adf69b38758c8a1fe0253bddaf218725dce11c72a9868b29e9404383dd40ab8c0cd85bc10c9bd1efd5d17939a9f8769be4cef6dddfff7d89cb7342e4dec3779d1db27bd3f571c02036eca7b0f1e017f28b3ceff1143a7f68fcffff17f1d7ffef2fe21746bf63f09dd102e4811fddcb95f81e0e780471c9028fcfbaf20067aa8988ef6c58238f23c3a32385b6ca83320b6dda25e6ebfcd09673c0d424b74bd7e35875473235c7a0e526b0864204d665baabdd89a94cf430a3453cefbb4742d6be68687f966e2f08fefbbf51e43ac3e5ac91f885d2b71acf097f1db77feef284d4671f9eb848db784aa4311fab36d1d73cfb3eef74c938d857efaa25f22a20db3900bfafe0dec3bfbdd3185d029916b52ddb3d94ed14825e91929f23d342f219bbdda8bf1bc8b49e93cbfa6f23c3a764401b80a407d2d238dbdcb8d9f1e97ca66106cf016cdf69ef87a61fbb5b1e088dde5956f1552ad45f7783cf966764f645cdc26b1221bf882f92e869ecc7fc8187bf7aa7b13ae546a81625d0edb6192901a6e37347dbaa18c7593e3cff646e4416e7792f4f8c3a7a970dd1af5a47afe33fccf3f557ef346697b9f6796409979bd51136094855a598d3d4128ef71a7bf85db60cb2b2671996f7bcf0af73081e1c340f9846e49e4e041a91fdc49dd85b4eab3ddff25e9ae98f59eb3987f97e4a037696bbfa19a49f46867b8ffff74ee37433454310407b878ab2820dc855844a0dd13f0ed75b6e9c30667504c71c5009c5a77331b4e3f961b0d257a4dcca46c3867ad55776c8a265b921eae5a3b9dd1fcf3d77ae1417b7297c8d4c617e26bf3fc4eb1b9d7c8dd837baa23b665f94f193ea781caa1b724d0587d10665cd1d4325de512fa96a94a115be14cbe3b8d80e36fb52a02dc4d71b00592994d416c7c6d23ea08e36be4e467d7dc9bb91dce29622920301db5bf16a3cff7de43e2aa0e7b375574cfda884af4e19f47f6137f8ff357f0f7b232d4e1a20b28ce2416403237c5a263794c1a53c7af8aa1b63bd66ffe3379b63eb413180fb1ff344bdd381f5a771a85b6952a75e4f97b0bc1eb8bea99c409e3c57c7c57f9157b1af537b5a82ac7d6f80fb19f5a70e8e1f801e888aac3d93593795ded92ef9839165068675782dde7b9d28fcd7b38df312a37d03faea4cc58df77d377ab6ebf15ff8553add437b5866619979cfe702905fe85b1cf9d18949b3bf9fb3c8fa6f7ae4bdc795faa0ffefe3729ff75a230c9ec6b95eec2f66c19515d1abfdc28bcb471be3d9aab839db0054d1991571dd7a75fce1cd3c433ff243a3785217f82d628707dbcf9e93b6c9b75f1019459919c1a98a81913da38278fb314fb5259ecffe20bf066f812546e1f5fcbce747b96fbfa2f56dbfe001a67fbcedbf3d76c962377b2ecf7f93391fd30d93fbc87d1255bd323ab1b9fdd2f30d53bd39197cdd7e9997787d5ce1accf891f1e543c881e3367fe17bbf569c533db11b7d05380a6e8bd74f95fe43c6ef0d707eaead209b9afb9aec5d4e714d71994571df6faac47bcc91affeb9e360339ab709e26cc29cd0e7c0ad91484590deb68c72c8080248db6a57513a9375c638f1616c60ec746a0e91c9c6293a3500f624e44ea9560a7da88930787703da03360bdb36bd5519b3dcce2795e9799e27867523cab85fb8619f5c57ce17300af0438fdec9d46ed86fb0dc4207235215c236c474aa85a503957563a420f687fab395ce5d545b8882dc5d0630bc60f1e3b0bd9b07274781ad1b321ad6914b9cf247694cffce341ac51b33f98fc3eee419e01abccceb2b86e28b90b278ce868d13505c81eb419b0fdcb62c6d7a4b94e73f904692e613c50e7f2c92971bde396ae29477db79a365c019914d45738b64277ad350bc0903266de7ad60ed3459b9089a94e4a5d17e7c7641749e30335cc75ebd06e6c3652c030325ac418b598a1f57de540ff3e815e9dd90745704359f8561c55a74a132f7d50ea8d980b9ff512f20befb0aadefaf7ef25e27f8d7a5730e0def59353727e2777b39e8833c03873039d19e964359fb91bd33a2223d1933ee75a9f5933f725c9b5d6ea3ae1e7c1e1decb9f3dfccdbd127719d4839a226a0f045b73d56433cb543ddfb019abad21dfe50adeb6bc128a911dbae6294d85f79a3031753fc3cca46f0ef703e04cc46693ceb6883dcff3beb39020184ac137f82251eef71e4f27e47fdf3d4a70ee69dc6a0efcde83188ab377bc91c07f734ce85a54bd113a8f1ddf58738eef961978c7e2b9b0076f982961598033c7e8615dbd597459193d56e9fafb5636df6397e675e53ed827833b1d7a86eb66a7f2f5f9fdbe378e52dd40631703abf0b2b8749f07c17ff53f96187736edeeefc0e15fb48a6e01827b371ff79037a547feba2d40c44552ac05b5afdafd453174278dbd0d4264420bc8f07302e47c77ea9dbd8c8b6046f7b9bf8c0ef75effee9dc6eb2643f2cd2e30d915b6cc67e2619b090139d50ff09e3b5088c0446e5f55097d0ea7d3b16eba4e5f3757045b85e95c36b7a378e1afb91c424d3999cc34576aaa054a4db0c027dddf9621490e4a3b7ebe30ffa61fd31ac14bc522ece4ef7ec892afafbc772af06bfbc04f63c3bde7bf4ee7013bed03cf981939d707337d1e1e1c5b9521a50943c388675a977de0ff73feff9b10570eb2cab7ee25fdfd6670eb69f0a7b0d6d3afae01ad61c1fb7e442e821a51796ebb28184f35dac311d18e63574fb1f5c8a1a2c661282df749473117ed52031b1dd05c1ee5425f53d6cc66adce07e271b512f446c3caa6f86241a56ea5091ef9f022e8d8f15cf1c39b57e1a81b01fadf8b903e768dc3c7ea80a72d053f0059ef7c3f8abc9134956420880dfbf6475f67fa3fbc5003b3f70aa7ce8ce4b95edcf58efadf861567d1d36ee3e53ed06353af7eb601fad7513dc3b65f7620aeb70dfe36ca224e32e0f8cd1364fa17715993e9510e5e840dafc28ea7048f1c44b95ff815e82546e1dda4dcdb9cfb33c385b84a7109fcc48c8dcc6e5ff0815fb597fe4dbfea6f2b4eda5be0af836d7f5b797e3eaf3705032ecb40a01746c155c595ab30ea3bc7492f98feb48579f609d7a736ff76f21e688acc78d960ea1c88f58cdcebe5657853ea3f66e1ddc0117e4da2c02823cb7b76f942dfb603503f6fb05d1fa67d3ebef45413237f70376ea3fea96368249511942f86d7ed5e97d7cbbce915fa91ff20621e316515e7f3417e916f3b87cfbaeecd2a8b9f307d6d66ff9dc459e1f8d139ab60b72a7d5c13f3a2cfe9968ae4e0fe7d2bb876b9797735c53c783698df264cc66566815e6824bd320b6ebab27fe779e6bfc88e4ba0271efd95647111176d027e25860d9ea27a6fbebf78b0fbb2dbe2ed8a110aef34d9ab64830b162cb3e072f55db557e5c5d2401f9072d1e176aac1db0e3db38cec00f48cc808dae31971f05b9d2dcf789117287a33b9a16783eaa4a25fc6427fa1c4ad8ea79cdba7c1ae30d4c6d9c13f8f593c1b1f673af76e8ec49332b8677af5bfbc6f701a19ee3dfedfeb77db23783041840d3cd90bd0bc52e991cb1cf74b5c9daf462c96b46cade6f12480b4a96d1d972bd66e9ce9645f72510bbb224984cb724b494d68d26b971646111673beac820d94ecb3faf793f6aeb4fd4f586e67109ecdb7b3475d6db89ade63e93c8b557584b521c19b48c00b76300d75c5f36a0d59fac725abf92e4382dd92555946c3848ac03928da2cfacb76cd0c4751e52d94b18fbaf37a110f8e6ab5fc5a32eba55abe546d0db08c9b71a6bff7f1a38c41af7dfa4b1d7247715c241e90d71a358a6dd0f313eb49d55db5c6497433e6715f9c3fcc1bbc383c03ea666b2fc9e2d0cf412f321eec9adb1afceae2ab1be5572e856b5e184f6738f10fceedbe6380de2e0a457e3533fb1ea4d391c3db4d3dec4d12d5ed0a6da049e9e5809e6e639a854653eee8aaeb9169e453949c1413211f48eb6ca46a9ba9c1958060dda426506166e2c3aaf00f15aab507ce61527c520d2387161b3cb7b5c5172b44c5511e07be6dbc6ce6a29771c40f4ca6ebdc982b75f578bce2551bdf36fbaf0b825daa8d6723220701b01eecadb390cd9b831449068ac27f6c457fa1fdab22c1171ff2bfc8affee55c1fb4eb4b354e9063084abe90f8e9d30767a7e23a28a57fefcefc7b77e65fb43b73d7f1be57b1ed8beafe369cc77b5d6e34dc88b1dc64cdcc8a30ce9367accc4c84d59eac2c728b2573dab4185192fca65a6cbd31e9ee884cc4fc3684cb19963a635972549984d2da3f22c680544607a0e6fdbabf6f26e3d2be77dfd95be4dddb84b88dbb2fdd7274170cdcbbfdbc8776bbd1681bc1506587f4685f8ee8041f0228504681581699e6e5dbe4d854b1b2dd16719bf70fda7a4b4f0e2590d5612a6f86ed2e2a428d0fd6c438ce198d48668396dcc13353ed84b91a98469e83d00cda7d0e1bf9fdfa80d42ff40b3585de8cff6094e7c5631dbb0e058676ecb125aa7da51c656b04c9b3c19ac9734935eac49847aa2d287aadebbc4712703e504a057119be6cd52849b85c6b77ac3fa0fa5a18ca0597ab3936509b956b57d917b7042fe772599cc58aa30234c50d54dd7db336f2b067b605b09e8a397cf8627d2aaffaa24b9ffadf9627976f3a416c147ee4f692d88f8a9e079ad7717e8edc77a1c2bdfb6d9d59e3a80f6c596c75cb3d8a6b51e9f7fbf071190fdb79987b21de5a9c1df3793bae61a68efb105c1e0a934a88a61d26257050bb669918566159978ee694a93428ca4a6cdbe9b4d04d721a89ffb8dbfba328bd0606f7de3cea8c402a43d32d1ffa6e3082ea95789896db5175088da065f382d36aab04bbf50a9b8a236c35ac1a93d76b9e5f65289159f6c26009b5983512e74c0ee578525634ea2769f51b08344bc7f96186bc80f48abac7df9df126b45109b2ca6e83d5cac3fd060ab95d102b168ff457830244e20ccf9b7e8ea4ec40e6d7fdb15d478e3ba466162dca3a884d6c9f6d482d0dfa532dc8a523278d0c14ff82b47ebeb9fc3c8bf7c771f70aed157fafcf3ae35019b0f288e684ed44d90ec9325b586d9c9156d44f376047c0477ed56e00bbdd21b8b7871bba06727c988a076d64479e6c98dc54df346c20dbee02ca27063d85d7f64cfce299d75b42363bc57f3e29659f0bade7e19fa0849387af2470f2b033eea37c286dc4150a0d76fde270b4f560ab2087c631b01d5aac95e1da1ed47a64c100df55b8d6da9a161900acdc543d4eb2e430555b4f559b65400a3ce5a062a940ee08e954b4af9b06fd71cc5d427c45e2e5f3cef85c34ce981c6f07ba9b12ab6a3b2ead1411a0dc5427438a590c2b783a0de8b91257290bf9f6740b2d70493e2ef423c4d752c853ce085fe75059549b78d18ccd7e128e907b376377c0e7b55df1e3e8bc00f88acd8bc79d9169095ba0c974e6c5db7c4ef33c4f148b8671b531cc094b6e5b4e52dd24e3d1028e13d9da248791c01e6976511ce2c810ad88afb035ce91fb39900965bd87edcc06e8a253a1c3fb73cb81f574efc19fc1e513bc2b543e3ded8cc9c16ead93fecedcd985386149755246cc200c197668ca73dc2d6b33cb4701e16c6c67ed0fad2a951290da2437f3e40dcf9375d9c4247eb48ce96ecca87d227523624e7cb12cce2d37a3b3a1fc6218fcac691df65c107d52e2fb00008a247e94339e60c0bde7bfba1b2a61dac096dc1f70682167d59a2f0c0de6135cdc6d8edb62395337d8bc1a570196fbdeb05e78c470c42280ef0f07934a68c753da30418ee706d3b7053fe497393413e22f72405394d619ba9e83965d901c0013c5063f8ae3471070efe98fce18aeca69e8eef6fb043fee2d90492eb41842c5741959cb68c16fb44331181e010936125f2bd2547435de6a974aeacaa366a3ac890581f9fb99c93451b93678745d515bc1fea21973c270103fde59f918a7eb82ddb2707e16b70f00e0dee9bfce781df5a7938345196c26cd87338811b2b9adc9e106eacf648021b54b5233768496e9623560e12611f6db54529b83542394e08963796c4d93664e4fc9852c2ea551ba3810f9174c94935800b6ffb366c90b14b8f7fa77675c4d549ba6dc529ce6a39de40265c5d96eb3f098ed41350545ef7b939583e9c412d85b9b18cc686a01e54b1de30b23ed6fa65a329f0d6258992e35d1d78eb06c26acb0987eb10cc03f5ecedf56eb5fd20f1f748e936e933f757eb3a9f3d126d097fc93b349fc3033bb207ae6651774375954981beb09e5b8c3dd96e4564e4a88cb32e16026f4c616c9ed5b6c3a4ff342f0aaca5eac2d760d1fd45c50bc6403d6ab01234404bf35384f565c77be582d813eabe7f2b7ecf67d92953fcf9867aaf1c3be2f0af1e3ae4fd2fdd35cf2c8bd3fcc2571f222f1e2a4bbc0dbb6d161594badbff66d0f99f2a541c90730ab86abada4c1eb265701bb9ef3e1701eed803b4c96ea70be70d6e54a07d8845a5bfbe5882e36f8c8a4f7e5aa1a8ee07c9456ff7081f779f9f3a57de2cb377f98fc2f7bc167bf3a33413f08b76b25cb767b7d3c10dd41b36eab762b2d7455e27479eb2dd2ed92e16b8cc86c7f9bcf86cba0aac99507f2bd6d198261ab70d2aea4e582b4671910f64c534712f7af6082f37de3ffafc897e20ff15871c163c567792c605b6de462518292850cd921052d168ae8899b946e955de5e329eb0de7f3a5113b961ea809a2ae49b8d0d2656dc0ee56cf70775fcbbbb486e040a6eddc6a7910987fd4837e2feef3b31c7a595ba2eb5b8f5ba7b7ba7fd1bbb9b2967e9adb1e01bdb0dbe3cfcefc1692d56198970b1c9392837d182fd5d5540e8acc1d8cca03ce310da167e1702ad5a3dd6a6dabb36d3325126fbe2ccbd6e241389b6414916f787a5378699e13ced13ecaf51f9569b723945fa2e3d586c13786f1cf47bed855e914ab1f6f0711e7a6d6b4ce2d042ce4542fc8a05e11e31939096651586e36ccbaa5b6886819d335b22bca16b0b22bd48aaeb14513e6e2548752bf20245d8185b9ae2c9515e8e49a9ea3ee163a88afdda7f8322cdc3bfdd723badd9a38d9d9079119a7cc7ac64998c09af208a59a6835597abb062c307d2a3b19c6700e9e97fb29414b003719289b8e6ac9991b436a338ba6607a90fdfd969894ccc8ce8e1bb8cbb6b761592029eedfee839fe566754f183e8d794a193efdd53b8df33e16d05e1fb769da65b7f3feb251e2a4ade78cb58f778841cfbfb2ea423f04bd97eb71fff75418e9e2b44604dcb8f09fab7e3c66c3dd5c3ed7a9d59755bfa82fdcf57899524dbea96a703bcc071faba3be764c2ddeaf368637ce5912540b7b66e2f122969c8999679c40d5e4ceca2148c930922a7c0367426135d8eada3e41657abddb4a8247b9464cf79920ddddcb067e3bffb7578edcce9b46be20456e0178c1ced56d1df89ba2b1b7131ab48d36d80d023f0f16f192e3525b14960c94f7a1ad466ed875d10e848863c0c4ad87104a6f17fbc52cc7d62bd23e0a2435718a790eeda568adc4bcbf318e5cd6cfe5cdd7c4ffeb312df27e7aefe35cefdf7c71790ae873887dbce6e2e5ef1bb7b6dd588d190da9fe2a07c7aa1dcb2b939a57eb03d1267a8ee5bf8704fc743efc190967237df564c3d70b045c2ec38ea501d0de1a88704ef6c5185e9859b982552cf24b74e00fb7f15753162fceebdda2ffe509becfd1bf7ebc80f1f5c7e9104707adb42cd4babf5015b71962529ca661164ad40413f5d467563ecc63fb74d64e8a20f0966ab5d5a5ad83ecd091aefad1dc6915c25446d3301b52ee969e54b3e1002d8fa358eb24715e8e27deab3bfe0599fb38e603221eff3a5516ef20778f24195b35c7acf9ac65729c26e4102f734f2918e0f6b521280f255516722c47f982ab382bda4ee6eda85f791815e1d9b66d64f7b06098d1b06a9711e5caaaa5e8fb4ebaf9a2ece92537d05fe1869772a7a7f73b6c0e8d451a53711435e6224f513ed8ccc274648941bd8f57a435db8e9b79633a9860acf472b45dce64ca9dcd06fb43d1008e8c43d726b1ad292bdbbe41acfcda461329fc6261e45b0550df1c3afd64f1d3f70a9f5e9d712933fff92228e274cffeed55bcaf9e0e29bfc3b95fa2dbd3b04fe47b3a088d7e2cc74f46f668ed2c612292a0019dc06b8ec0fd017bb409944dede95c25029b8aa693c1524289d8826b40666c54f023d5a0a07d16e15134c0993877eadd10a94d2289232c1ef85db2001e3ef500da3aceecef3d67753ef0134a9e7f763d5a65a87b7b3d99cb66395906083bdf59100d8889b34b1073bc7387f672e9e088b908eb8126d0d5809067add63663c7dc34c95437eddd18a6b731d738334d0c9bd5c1abdb6927d116249e1195612fbf6f455da6fc77c6caf9c80f6839ff7d62960f0ee7a0bda0cfae8343d8b2067234d69813215ebacf238ae0840e338b72fff1e6dafbd4c6be22b85fc77dbe26f9f1d7a96a6b0701ceb3b3595b7ac14ac795b05475c1c3e2e3800e14d9732ad70c8f06633515a00b98de942d35ae1da28d879c258e296ea2a0b3a1e4a43b144b0b0b1aab1923006e42479d687d59aae71e4abeb000ce067ec6c9d3cf13523a2c008bf1920c10a101a98882c602e96c5d6adca28b9a3a1eeb8a0a0a781a8f920cf2c6d222d6234e5bf13c618f2a999d08a154664ba992b35aa1641e1e0f038f99d3d4ba935478f858af08efdd097059dae753187918f5191da7d3eca7b13e627a57d9b0432ff0531085b0b474822122d39b36f27644c7d95c5f86fe0d1ed19d4bd03b783f688f92846dddcea70a1e4ccd21b731680c638c1cc7766ec709bdb9f4fc9a65bfa0c4ee5e768e77db22d1146437ef1f766b5bf1067abba8715930f5102e213175f261c3269b25b272e83c1dc08026364db34c565c434fd6e8246847c5521b8b4b091a4445904e1a7f6d4119997ecd1839899f9e154715c89eab06d0773dbba87dae637afb8ce6d7f8e371d0132a1fffec61dd3c63b200cb5cda0c99ed6cc64c56748be707995ce136bc7136dba4c2276ad2427c9c0999a81b0a9dcc899817a42db53f4a187708c4649a6c66477bb455c60aa6e3db69b063be966613fa56169f1779bdac77fb771467e1a996eaa97ec8dfff795ddfba8bf7f8668cf74ebf7dad42d62508b877f5e07416ae5355ac69c6f1da86d8b9d832ae0d1327abd8657138cc8adfaee29e8130aec08395eb077ee4f6729018d94bc5db47bbf4ec8d8f2e4d37922c368d6fdd4a7e1cf281a14f7f74dd42d6e9457fb7df16b1ee0dd6d1561b094b152e8a7178400d665f46295d2d91c2209b01b9194bbee4a8133a9faf98b9629bbe3fd72c2646f0998fef85c6779922b7ed607befc2a74b1464eface92f39ebd9e37aceac5b2c7373f20c2f907d6315d315cf4f346300c150b2e0a36c4749d3a9a2cd36f9a8fe7fd8fbd31e55d9af6f10fe2a67f65bc2c93c3d79bad3228a130a222abeb83accf320830a2ffe9fbd53a8556a4915e5aeabef4eee2bd9c9063c6ac15ac7b48e35fc9667f5362b2661a6b462f19aa4d73b9d4f347d335b246890b3fdba48b6e69eddb169b89f715c5fee7434cd9cc633f1d539e7852170a1da48e17c793ee474180709b14c2ab298f3b83c83868ba0df9b93cbdaf7b46027a3c472a60e3343288ac95e374d9add64710fb0f99aa0bc3921ea30991e58fe10128e04b3386d4ec23cdc1e92d56b06bfbcf155d99703e37fcedafcf3699465a0e9d976ab13e2a5bde142b491e2f912c4bbe80d48a04aa64842d46814e3eb3162ba491585d8e824f7ba8c880cb443ad28be08897a25c9f686ee85a1cb5dd74cda19e64ed7c4124a89844696d40246c77c7c4cb92848c90143a35bb7d0cba36c610b7cbf2804412812d3337a720fe9c715aa005a7f6bccd3280422dc14f3433ec17cade8364732b08cdb63ddb197e5d150bd48a3b906b1ef65818038914fb27d6c01bc96d08eee7953c60f7665b930a44edc6815687b61bbf3177ecdba7143f8ccd3fb6da3bf7f6ba9b6355f9c793bcf2f8ccae1362c248f507b5acb5479ecccd6b703f765be6e87eec77d3378bfe52c0a993418f227e268d45371eaf427da6cb748d2b5f9dc23f684b3338a405b6fbd34fcce54df398ab4b4e9a76f879f379d92450d1b0c55af8ffa404d7a22aaa33b3b8d3a73935966d98ab6f8370c9d09bff374beedc6d6b0f67630042d4f4c808bc19250421498e055b6ad9e3b4a9eb0f5d52af1a22de883ee3b53e795a2931d28f2972c17300b511b33a46fea844e1ce9c0c4292e78c9f9737dfffeb3b6f96943fc68f98bb6ff77b2b7d2d877f1002060866663939c1827c85cb0aaa7982b57c5983c8074f5071dbc6f2d0dfef6097fd1c3fbd2ba63aa3cc3f07edbc7fb898c6d95c380c991383e08819c422ac3671ed2ef6225c863a4951df4e614d7999d3c461a388318011b0adf6fecfee9584be94cf2e568b3758e5a7f3760e635e0843d1e46aa2122c51b4777f9e3684cf4c8e1723c50c9f17a5f8cdcb1958ef6630a0aaa41250d3487ed13113b188663f560bea8f169f65d08ea7fce48d0cf87781e23fffa6dc615fc4dd97d61b33913bd48f05f3f07cf84be97628aa1c66248ece1dd3e674ad65bcca7ea2c22d97ed84fd8be402c98896d2972184b33767552cdd514483740bcb5c3885682a9892a6a38d0d59ad80f3c7852ee968c11c8af9903f4f85fff036efbce53e3c5ae957945fedcef73ae15137a79e119a096e71f5699767ffbb5d5f345067fa90fde48365df076013654be5d69778362b374d042d3911cdef6e81175388e217ebb1bbc22c2b2f0c2f324446eaa7974b193dc48b8dd42f258eea53b12ff997883c47fbe6ccc559dac22f9161ec395064b3eb1f10fcc2926f8636e1c6ccd1efc0c2bfc229c768874e4b17e4e57fede283769595e089ec97461cc3ae41a14f67b4ab01bc73e8a410eb5b256f3fd51fc6b73cfed7c417f6cdc398fdf342cbff263bea06a7dd0fd98256f77e71adfdf4d155ac645718860f96c344797cb542a57a9ce12734456ba6c58b9e7c4605e457ad28a97f7325b37a4cf9cdd3ce8c6dc66b383243b335715271cfa25a1e18868637ede438d2ecc155953c7acdd0df12a6b1f84cf6043efb75dd87adb5f806565930ec64e93a541f9764a69265d08f8aa103c6d73d0871002e592b1138340a63407e3885860fb16aad88bd5625ef4028d878b05ecf9589f981d231b1978953fea74fcbe5482782e8d5780111b8a8d18aad80091efd10e1190e253370b7763ae77a4f3603459bbbed79fcc885d9574e400b4b4563be38be104ef64df7979bbe9124e8080bcb9e013332e4ea5cd564318f74a943e6de05aedc35d190abdc8fb2a82fcb5817a43f99dadcb7dd7a1eaa750398c9364e12d80198a20d66e91c8559d2ec62b617b423276bcd24ab50a788c5daa6249bbb0629c7271cec2f872491c127c55d518a05508552cc9c9da66f6b5da1633f8592e416b622cfc5a90c485e8551a8177ae9dfbed527422a89d8dd17085d359a257db605673bbc09cc9d8f3c3df3d2b455323b1cd2afec2a42b12fd8d8522d19f800a3e2ffd15916590680b7ee2411496424688f28799bf1a13130109a005827b394761aa30a3e5221b4114142e874ee4d0f1b8bf73384cdfed0efeb1cf9f0a61c161c7cdbc57d246a77ebc07ce7f26847b28fdce42b821fc268c9b5bb0a1f8bd50f63271dc90da76144404a6e2358515433e57dc6857e29a20ae196fb455c3216c40c7931fc4dbf9a8f421c4184b3a40aa239241397dc2f31bc3087adb2db03e4d0f9a2bbce63ed5b3e4985b59fe8e60893f865b195aec95b9f551aba739a8c3300c3318fadc439869b1f30ee27a0fc31a97116a67da7b8272039770d7a21b046b034b7f8bbf8a3545759eeb50c71c7cd302d0d6394dbd32a7af54df06c1f51a6c687d37ab0b9e9cf45772ba59d2c50c057804d0525a170173dfe5b87fccdbf346e8d718c1cf3ce06043e1fb01bcb476a7c37c345e1c687a18aea8ec646cc7e33e6c8b053edded77fa22f40ed5a6d8ce0a7a209f68bc36b140e893de94f0d7968cee0bc0a1ec42a62a0bb73d98dc1259d12960e5497ddfe7319928f982241ea943e0a747e099f4773dac4208c730476b41d98bf96ec994228e463ea39ee09722b63f10bf917f917b20e1afcaf0fef173b048022bfe284ef623c8e0a701358fb27edd57df1658d3d9477fa404b2b07094537587f0fce332c962aab237eb39fdb3d3e8f3409bcf9ca27fc3e9d3801bf453c04ddb9ac1cd4c01f1d0b9c020b8b58c24d409d895c8623afec30afaee7d64fb23978f23ac3b970d65083cffdf78553a1dbdf7ecd0585b72dc3b10f3a1b46716a3697f1ae31368e2fc4a7dbef77efd8c6bffcf3fdf9583f984f6ec6af94701b9a7e5f31e8acc3d2d099897699a64457e8e9efcf3a9cac43f5fd49afb98d2ed59188f1fdebdc0dc857a5355ee72dd44ba7502908e78dd5eac07fdc111c71928da1c1645b8e4c6936398ff6c90dec9f02b365f9b8f1fe421f0e6e649364e0ba39aefdae558614219b1e921bcf0f68032c1315e34da579e9f8cd9eba2fbf5b078aaed9c77ac73a9de363de135eff02de5f7adf1521318e9e01e6e1238a8c50af1e66b60e52f8ea3f96c564d02603407d4f5660bf46703d2163954aea2c96eb193d81c90c6d669e8bb111e58fc2aa2850d73caf0419f916051d7662c8aaf09fe55e8ae8601e83ea1fb5395f06bb31b55b3adc95dda426ba36b16e4e7168f55fe1f8bfc7f53e0ff071bfb0f0a47ff55f9eed6c2d19d2699b81139ca469d99a33b7ca0ecd171b5af47b551947fbd31fc6ee9e72fe6e17b85a467f390beab56f2b3697826fc3e0dcfb76043f1fb59385fc5a378bd34d149bcdc8b7df8b001bc85b45188d470395ddde63c3a2039613b48055b31a4a9110e1927b2311edb096ecd53639ef13ddc4866b01101c393b53971695b14fe37b3d0f662f3522dc2f4b2a7bbea9f0b731f8aeea7e21f51607ad9b5ccf343d990b73ffbf75c2be69a7df48533ea2cce4b8180877cd7b81d78147d29cef2bb97bd776f7b932765be9f76b92dd20c339829f14a638d711d651b2a8ebd6abf37d54db629274a256e46a7a106195446b93239edd3c3dc98dbb3909d8f135136082250057e3098faa23fe1fb337d80bc18b29de8be6514ff9e4df33713b0a53f422d362c13fc2a70fb3543e53de946d6b70fba390b3c7db58cb43ca382d81a06a433c5e3c5d2434e45176781aeb5866dc2ff22af98e8de28369c341580914e363a62458af84974d21456b9e141de1c073b7c9e0c304dc02556da7275b4d633ad1f0c7dba1e0940a46bb45ced5982d3779b6a4945ea2821558a35a50413921e1ec12b4b7ab10850b3105cc4f2ace463a8e5f9cd4200df812234e900d7521556e415c54db9ce3b3aa6657bb105a659925a5971bb28dc34f2f2f348fdf3b4f463e49dde66e4b584e2a75a85a9961b5a6868f98725ec47c1f29f3fb1fdc4f678c2e83a5a1ede01818f4f9ecc81967960a62499d5cc5425b3554690736150e9873882f285fad70ed41c34addcc8bcf43646bec359c9cb41cd30ac3c4fb27b0abf2fc9e7af82c0961fbacaf58c9ee3c6a35560ac73d1de9fb0de34ac8a622cead404d856c1e830115196d1779c1fbab37132d8e3db644ae26b87d2935db8dea85372a6a80c6eaae26040cd5c7817eca6c6dfd7be0fde36eec4fe73ad5bd9ad3f4cadd0fe5fe88b87d79cfbe1e1e14ffac0d797db0a1946452fe9fb52b08ec66bf558967b6ea8e8dac2e2fcc8329162c18b592eb8c5a487671c53044e0f1e22fb09e62c87cbad3152d781696922014b443680fcd9ffb23ee82afed76c08776fb848fe5ee81d6c096f42472d2f27367b895f53a995245c358be60351c9d29c01e4ed96309874869d062c805bcc28e64b4ea322960a46a7d111efeb7b99099015ea2e4658e4cde898de149eba2efebe2458ebaaf2790b691ff52d6d1f3bb4ad96588baa945b240eb6c6b32177e01c3fd1281aaa17b5a2b96e5084ba64b61ada01aaf843a2d1302b71a1df3fe5512461dbc38984212565c79e4f487314d96047d6835c77dc6716c90298ced292194f5632e3e3dc723dc1348e1b328620271a1a76c908d2bfd016ef8bed7597c355497cd30d1b1adfe98618279d56301107276722d2a633a99410500ed596ed10adaa1b59951660aa07665b4eca6bf1c8b784dff8b9b9ed16919c723d733129f61214a136244e079a88aa188e72e9f324dd6f74bee2685945ac19d7e2c8087e132d772f12dbff554816ddf6df0460fb5d4158005e991fead1128f97155fe992816ed2f964860b13ad16ca83cda0a535ae7ab03e0ec4e5d175a5121bdb3b0915e5642da67d766247896c2e4e2591c549610f67e1416df37b7d23353d2c2dddcbaea5a01fcebc866b1901786b6b7a28bdea26495a3d77d11659653d51b31ebac273da0367893b9f6ee7de6848be7548730136543a40d557337451459353a4650bda3909191cb97d375f30127f74abd82e4b1c1046abde86346a7bcfb865d467a96c9a2a3e6732aae271a28247755a95e9503e2cfa61efa8479d7c915eac6515689d0a2b7ee3bc7dd9455e5a771fc937927978069e897f2fa58113f9eb9910a3684d593c7fc002231e1de578b28d86237f2caf546600c9443836561cb6381d61425a19ec6c1e4c86b5a163c6bcb41473088de7b8029814bd86eb053deab4107f8cd3e78957c40b390457a26f22b95c820da5ef0501eda7a22285da0659544b02d9587d913d2ce35e18ef86ca24e8cb3c31aaf9b81cef5da79a144360c5e0e1308e31ccf2b4da5f46e8ac375a57a853ce636d8fd35b469e949d04117f19698efc4bff5c0cf165c2c4972873e45ffa7b098c0b5bb5c559b01929be1148fd803ef581d9f1e44ce9b96a6cab23ef3913473e2e445a45bd6ab3c26df490efe264b15dadf40448886a1b8d9509eec40481d1f2829031a70bd0a39e98d57700b908fdca70f820fc268d8f3bf04cf0db80cf2a70231132d7ab9ce42ace828741166ec275357b9e8cf7ddd25c15e765f7535deba602535c340bf39f6b3e2a7e6f98d04be76c5024ff65ee7f49cdcbdf3c2cd86f92f9c03a7a6bf0506cd83392f8f01ec003ff8bff8bdefd9ec4a0edc55eee9e25fd1ff471b7d8e7e74d96b87f71a61dc137699fbf17bb7fe91b8fa0f7f14508f933ebc7590eedbef87bf1fcc4e6a1974e63e9d04b076ce8743b8ee87d6a1066e6449bc4a2de1301649014ea61bfcb75cc3242e2e891aced91ec64a518f3e49892c6214f1dd8dfd6f17235e5fc081a503dd319b26bab27ae85b931e5f3d15dd456cb286b1f69cd2fd17b607987d35ff45d4ec16b27eee86d158af227b0174fa589806bc72b33fbd83b44c7b4673276c40f71dfe2d621ddeedaf9eef493c47e52b6ad2cd85df1f7ee0b4b43b359549aab6697f9d626ec27c194e7d005a98d8945ed0e4e291f4c6986aaece7d1f1dfac278f79b167e40df87e71b84363fb643735e3fce167f8f1e7e254fc791a44149561e1190de6db259088fc640bbd6d02e65676f08c1bc0d74b9df51f4cffb654e0cf43f6d532d95fa704772c99fdb62cb873bb36166b7a83cfcd520b8b3d3b14abf50220704e9e943b5e65f62b3c5d66094bca8c85799e6726e8581ef7099759a2b938a147d870d41f7b83383f20a1b85d60d3e20bd3d0b7d32009bf702bbce829b9106d26c2f9b29b77449b430800a88262e5981192290ec027ce385a26da01cf49cf34c302ad53aac5f95749b9c82b30e70fc4df38bb7f724e0bef7028f4faa538e2864e36da5223f4301843c429ad61aa96472c0cafd2d2d9bb915aac23614112081df5cba5902cd461680ed81076109a2aeda436b591b3991aea5027913defbd18cdfbe82c7b32d78d2436b4e2928c7ec109783e92dee4d1a6b1a22f25ca9e495e65fdb65f7c9b1cdb0004ce4d3df61991167d73abe095ae6d6653743cc47c031a249caaf7a99e566fc822c156cbdcde9e72a3cf28db054b789c3790b2a01aeb9b14956792a9e180d4d3a8d8682b4cfbfd5a7cb712238f2bf14392f1d94575b79e36a73713cc5d2d0c93e39fe7e8995e18de84517fcec1fcd2359559a9a515a0155a911517efdf79f71579aca566a63997306cf85ffa9ec6c7ef57b7f793cfccd3d02b2e91737f3e424c6fb5c0e4269609bb1b6c1db6814fa2fafd74c8fb5740e0c383eea9918ac630aa92edf6b62a916e792809cfd44ea78d27fd754898979f8ffca6a6871fdec49f45856549a6c56da7f0d7e0602e349be9dc5c75818141c0413eb1a0a01f6cfd6325666ebc711281d8e6defc4477d9139a7c05cfae40ad75717a0df4ea9ef499ab9b075d41b000aa3fdaeef9324f95189243425b50030d3e6df85e315889dbb9dcf347094e4fec99296e0c2176306c6be1753c3c48476b6f40651d10b5bc18632302edabb1dfb775c27d713b38d7ff3edd781cee8e688697ba5676ef6ebfdf2b324b2b2cd0d572f7f96a671dd2ffbb39ee16496055cfdff275be78aed9d64db67e83cbdeba21bdf7c7f9cb7f3515ef13f5fb11709155479cdf5c74831aee2da7ba8d674ba940dd55da1bed639cda6261380b143e064e83493df38f2a3d5be6680a1fa8d03f8c8941952c1750649db27919cf077d2fdfe5807344eb1a793193ff71c69cbbe8ceea70d3c6b4f2e763a1a5abdbecc38f147fd17f7147fabe974c2befe2c368f0dabc64819c6ccc1d1e6b3c30488f75c7986771c3244a053713c2981bf67108860e262124415af60c6c09cc54fdb8ea313d42359874cf28aab8b7ed69ba41177a79dc122f62583e99870fe12af9158ee17394ca0fa717d206807623c62c6fc313c4ff766e65b976df6559ae359068df7ad3c6563f030e7ad54beb0a38e4abf9c68a7ba6b45acbaf59e96e102e1e56b5b7bd2c89decd78ff85765a932ed16c6d52fb390ecb03ed7bb13569ed0ddd6fc1650212f54384daa9e55e83b4d08e04e1b0ddafe0f479b6d98fe486b42c23e711f44473fd764bb9368834e3b9e66a85a197169ef1e723eaa1f37438e3185e1073be2c0272c34b1d7a6d79b4f0df6a1a6fb4ef3bf6ed4903bbd341d7d83138baacf2d1369a64ee3ea0e9613465d77be84014fb480eb89a4a099c8704bef285b83f1cc67d8ad96ee6ba03ac0b8dde901b1ddd17f26e259fc67c30c996697f3878f1e8996a41f26ed226be94e935d9f4f94c7905eee596f087349b5b10ff1efaa5c1de19b0a7c1d6a126474d0bd8addf5ba9478843d49d8b4b8adcdf42426c42c7782f2f4551f61005633ccee592299fb08a916953f3c48599ba9f9ce43111cf0f1c606e44f4c5eda03de31665ee6782651459128345021a6e96445e195dfe00fb1781efb4b0265638b3424bcb6f8c726d8ef6eb86f1bc8b5e29787126f9d63967a514ffbeec0502eacb12ede92cb93ad445780c59c1098e133ad48f2f2e5d3701324f82c8efabb83fe01f7b7973b2ff56ef3973676749f4ab088637743f64f876d705bdb0a9992f19db81a2f7a7c520a174773fc84982844795896fb5e4b00b1750c0ac31d75f2b43a83e6ea829bb36e529ee8c0b48af8f3223e84b629fe8c090f0cb7c97ebb02a4ddbc6f7338978b1699d92f64896bf10ca85f4875c2e0fba8a0687b2c5c0a149df3b14e2ca5a28ca8a44fcc902513564952da145b0efcb3baedad3f866e262b3b2f45297552773c08e8853698f037cdb1b1a83636141f4a6eed3631b42ba6014dc9dcf7e0f61ef83ec87484e97d0bb6f7da19352b560657c288e98328b70d448a9f5485ddbe4bc03e2845e7a61e135255f8a326fd2a6db91eb5f32443f7941c3e4e7c7ddf0f9dd9d93588bb11e2d46fcaebf754469085b636613971d52f1af1ae22ff2f746f18da1b7ffbb710057ce5a1a204998c6f82620032031d7d87291aa830e1c185a1371df36fc9097acbc17a21078bd02cf843a44a9c0b5880f449e5b682bd4700598ee853983586a6cd33bc01a21daf1785ce7b1a3444e9ada02e1fb133ac46ae2b02052e2088db159ee6db5a38ee41b78ad92442af5d8d732113ec557dd5b4edce41867ef3bc583edd6712a302d4df3c32afa109ee5840d32cb9fffa21ed3f99d4c332cbb0c41fb46c7becb7008b312bc76daf93cf7909690e76fffd2f479faf197a9520d2ebd91c4469965565c84d5f3e3e83999ca02bdd80e3dc72d9eb7cabc28d3ec8b1e4e3e589ff3cc7bee4abcc066da5e68c5d79ccc4f22ae103abe395e3cdd89ef52497ecf4cf441f63cbe2f375d0d43bda961aee2a1bce9ad4c02e2fbe2f294a17df7389ed044dfaab241e52f47e1f42411a4896f27025f4570d9f3a6317cd2670b7e116240b54c172b0fa77173dd1f18444fa95f8b203492f04d7ff492f89c51dae62eea9450e358c51993e5fddc45defeec6af9cdcf3fccb4c93fd1861f5c0ce7bffea84c013f06b09cf176bff88232be7fcb172adeb9d3bf4cec44ff6268bd2776dede3e71433c2fccc2302b6cb8a8e2883fa1e42e898049b666ab62c6e691d2afd73c0ae1f358993a87ed1cd7076b1c5d228365888c2b6f041987e198eb216ab1474371c7145b4ed65c434a5f1b5e4f923691ee2b516c25e01535eebc7ec0f7a7994fd5483e2d74976272f759a33f8a4278a0f0fb992ab72f80c0bbdb9f64a778282578063e4d0e0214c67c5ed14777d91fb906b5f68c89e6f7fc55a63a0832656a7bb78dd5718107c4100876c1d18f33775acf3c031acd14dc4790c8cf6b948728e2ef1325ce3513dfd6f827e9d577f5f95ab2211e8ad69dabd2fcc8ed656861084616186995fedbfbc00de566b6dedc77c3ef9b14e33199c4be85540ce14ca4644d7323560b90a8035af9dbfbac0c7cfb2f3fdb09da16a29f6ba10fb42fdcdd3ce952df0801dd112ed64ee2cd917e9af4eda15ccf6bbe10d6d2ea2583edf5e5f99f4fb8185d26f3ed9fff7e04dc3bf58bb09aebeef1706c590ee81eb5975668a21da0433683a8b91350e2b0f76a20d0a5cb1e2a2d3da418bf3e382ee5956eee40b88bf737ecb916e36f586a2e6a7b7939d88ba442f6d798b97ceefdfdd1a0389b635bc5f1d8fd7f6fc97ddeed1dacb7089891e566e205520a0934afadb14d7fcbc7f5ca57840e70a48616592168b42bb7af1d3f3fc836fc5c6fba1d442bff887ad3555ad7a980c626739a57b4b19a4c6aec79c1ca6f7a364ec0f7e469f431b9ba4cd36639ca6f0f082dfd1e59e11782c25f9c0417aa57393562c2bb4c004459fb1b60e48b4329920d6d69005b4bcb81723eef80da7a350d6ba9f7cb1dff4eb7e1e8fdae6b8d773d5f1dcbb103b1bbdd143a64d67aa5aababc492c74085088484d09891f6c8174851230b7d36d05112422cd16d3545c02b319596ea6536c2a1c857e4fa67089c59408694babf8eee4fe19d2f2f14cf3ccc0fe8038626ab9fb6f644589577f00c0a04fdabc97d2c09b10e3964178f7c25653df1955937cb9fbc2eb6efd710bde12ee50ba2bdbe26333d587ae395f50fa8a75655d735364c0a6fb9a35807d76582dd7cad0de393dbc2e346a650aa4382776a760a0385c5c63640488f68637cc2dd55bdbc3cdb89874498f6926506ec5b9577887b35a9f83474b4f3523b8c09ffc2af44a87f73592fcb6555700969218847512c8740f4923e6206090c7392b638c9df6a353d9d712065fd4b4558c31c386575e02a7b457079caaafc631cc942256294a4602074b1b90e3c08f57aa8c935daccc6f3c84ed15dce17f9157e5179e6bb85f2fc133a96f61ab51c718dbc0a05757fd9d224cd824ec734bf7a4981d6a443da217de0f04fc156be51d6a6143a38337a5f0009b09d3c9dc380ef162b3a6a145b65a1543c4d979804d0ff761d5339332d34263b8194a9b210acc9dfe4876fda0af03e3213bdb337ab93b0e909311480a9e5a72f9da39fb11e3107b34cd7c896f78675a7c824a483496949f9c991f69b4e9dac45d46c64f7aecfe156fe7e6bb0720f12947a3b51f257fed1fcdedb8c0615a187bbd7d7f8b27ca34771474b0eff1758e8a6910e19aa4ea7e04ec069b81b1c1657b385f657829cc7791065b156e13a133cb0216d6b6b02424c7bf3e39bb5a0edaa1e6dc18367e72e875b5ccb4da11cc5f413bbed03ccf93b7ab2e48c70d7ac98af6382373616ccff1cb6594f450376227f1290d436109c010bc0f6b7fbf1bc8e4dcf4369cb10bfaf35e8067e270c7483491a1541907de7ceed0bb8577dc6dd9d1d1e8e277bc4f817e2609ea252cf71bba8d34deef40aa5bb97b552fc3cdc8b4458651ec1d85cd0d850be7137730e69c88e1923dc3bb69b5d58700d5b31c0febafd7d47e2ff4a7abd57e720a0f83432cb12933ee01e629e7e7e4690ac59d0a851a6e1278a6d66eb47cc5077b25dac8e27cd9b562aa631fb1144da4357a1c1400bd2e85709e8b7effc49d0e07278ea0a006cadeaaa46122daac87f47832a7ca9ae1e694c7d6c052988ce5c85bebfd053bcc993dcb0b7e21daaf2ea2d547cac4270bf35df187279e8ef74c89b7bf7d88f6b773eb60c545fe6e36a71e1d4160aa65d7307decd146fa299ce93eb436072f29e1ef66d027b675f0ddddf44913ee62476dcef65e0e6a7a9e84659bc69c599af96ece7dc4d9fd53de7c5f2bd4dbbb53edf72204ce24cf63f318675d23027c8c1e1c1604301b6d69dc0f13d2c5953a5a1c3d58713624b259566231a9f1bda69953c252317625132b159211b930302540c3b140ae44ebd00b959937981c3d8b26e16e53344ba2a634b46181cdc0f9d53caecfe41bd13c3eec5aa4e3c4e96ba7b2b9d50a4dbd32855cda9c42152eedecc952d6284c3f229b1dade35b0149283b1f96bab2e655630020bd79b1a82793c18c4fd584471912a9e11db9e88526f35acc54919f83f4be2cff6e78a017db49ab485fc18cbfd07c93e3f90a6ce874c0adc8793338ca7d2edd44220dc7466fb5a53c4b23b88d64450ea238fd0d31a18bbdaf60a17ac487ac5afa343381fbab7512f52876601fe49d49e37c340a1240380d25a55340ce7db471dbe0fa79b8ea0ddd461aef77cd70ea5044701ab8055159e48e8f8e071c2e156e57175ab54257ee4ce2edd88756143287b6b5b5b3122cb6693ba93654cdaf72510d06958645848d19bd2a1d017b8b5217998028e86bfbc1afe52c185e6694a196817ede9ac108dfa5ce7717f70de546e037f74dd9c90e6818ca6e8acdd9132a181aa013b39ca77415a1b174b42908153915392e62624d0de6ab23bf8e270b65a32f752c31fcc396724f4cb187aa55cf34c26dc4e43b8c76e6a515e19d06e11d90639b545eb0847cd07d93c9c75d23910e0690fdc2153568e85339b952e9116de57c4948f35410a4534d312e5066753657e76414030c1b9f428eb70969076cad3e979f645fdce7f29220b78bdd6212a55699c533c17a2d04e5b668f513d5e0198625fc2f7a7fa4fbe459bf7791165ae11997e4b23fffcd30958fdff69b30950ded4e252390dc02967e2e4e02310a0f430b60a7d9e26438cbff0698ca1f67e919a1a5c5a0d16aabc1eff4aaee73e242b59911976b10ff5479e27970e4ee3426cd0a0f33951c8e6adbab106db0d83a253e1be71b54708f3cbc66a7a691f0c908dace868e3692cdd98acff005cd5a648fce306191ba036f4cf30612c39661632f56764dcaccb0ae99cbff819bfdba458e1e689459de1a5889be66f57f27db48f27ad39c77be357be5089acfd12501e447201c2d19c9e424465e88d0ec255088ccca8b24b33ed87c74897e12489e7a716c655fa103be32b63e085f8472bd6d30023b8c30449ae1048e47c920e7d62366058564df8a92038451de015916c9d4a6e61318ef9130800bcbc1766e9f20e9a01df4112925bc9973f81c178e15ce1891369e2fa1048dda6a077f124aa1e9a185b56e41c48b223993bd08e47c0336d4be17072539e824b352448600091b1896b72b2483133727b777608fc060692308891ef0dc0b26d921f185d170bf2d6869bc76355a09474ce628306b6dc4aad831d0aa56f7a1f29a27a531a77d04efdc9d6b2f21579f40bcef7697c6e278f4cce27ab06e77d385de7bc35f34f25fa95e7aa2b9ee62b04740e81417e4340a896d50498a753c2e69f130db0388df210eda08bd544fb4ccacbec8507e41e57b277be6e772d364267750f6d0cd7c1c2d0efda5456624c6074c2abb7670dce6eb62dacf12a1671dc6baa759308a44324dc04ab01175eb70cad91a2be283bf1b0ff84378d4f67d313f027819d67367f2da714dcb8c8f21719f667ab20cedcf1592e067111e5992e7609e6ac7af2063884fd1489dc5ff41ff4dfe1f7720f16903681955e40c5e40e546ac211d0714454aa66ba584127f3df8eb32c3f771c1f8274084b709e95aba163ba09144917651f81e71cb9b8647d77bb775a1ff32dfe34a5d7bad5df7a35f147a43b98165b00c0d843fd5a76b11343a37b3035bacc75b6a198848814c60da8429b19a277f2be8fb61763dfd3d48d0b11a5c0c4b8b9e6adeff5c14c69b268fe1914d93388dc0ac8c3fac734f2b4ea5a0edc55a183ec7b13f77bde7c45a085a27eff951e09f7fdeab2e5917fce18e20eb779cb6f5ffe7d775edff0ff2107873f3242ea16524f8a9c40c10df98236b69bda88059910ff7aeb9c05ce96735a96e27581b9f6f7be06b679c77ea10f871dd44947438d73490039b5da80f4474918fe6c1361342825703c89f06142cb0b1432a0aae8ea13dcfa9ba6393aa2ee28343b41fcbfab69f4cbce53407105d4e87a3b5239d0e13841aa2a1f70b41a0696e956672d1d99f9754abb4f03d5603bdcd4f6f1f741f7ff2fbc86017da1078bdea8a0686802376448c557aa48ea3c0361519589da248d392d17afc72185fe8956db13e2fc62ebd513c2b10a5d72566a939060e796029f30c37a3cac171a9ecb6bb1893650ce73c4eb46b115e2fbcc1b15036e100e82dd1c1d8015cb93a646347516cd774969012f4ac0d2a8d1c4b04dc4db6cd29ff35db5c8b5af9a078de54e0fbe4f538665afa58c4af45f849fccb46d2378a8df093b89361140151bf6f1c23d11eee42411b5769cfb58c84b6e7f6a803869ad166edc65fb3763786ee04c4bfb77123a0bed7189a715759b812cac592029c430d48829dabcfe37f1f3ebc0d82027d0935c448b4e6d3b52628b74388c79e807c1e50a558aef078b95ae3432a14ad949dece83d3b9f4c38b69c9889e573d56ceb44336a3c488099854eb85d690e57c3828ab2fe7ebcd464ff98e395b3ecaffabded6bdaf2ff75f636ef2f4be827acef6b7158f4b176f3fee6ecf67c8027a605a6891717a0d6ee6a7a6995b9a5dc48fee6be1b50142719b39524911b20a993031f701ea58bbb54a8b10ea5959e6437fda21fed8178c3dffda36ef88832ddc34476373b5681be5a698293d6a77d2fdd61d3de2b0325d2d2af72b92e67f6c7162d23236cb59e612f8e8873b4ce394807ebb6eff48f5592ce8272a92e08cb4307699d8e5d72b69f0805291480bf7317c47a4cd8d07132db65de561a602824cbfb3aca5c406281de6e45237d9fae0e00551f57c986d6dcdd8bb5a99aef3692f86065c5bbabf17e2a364d6ef0f0900632b95dc037e49e8f4ee61533c12de577815feec186e6f78297ec9e5298c06168f68aa164f4e97ebf520bc456372779dff3a6a8217364cdb1f41c33d27cc0f60ecb53cad40bde2179481e8d266a6f45d3905643e392e37bb37cbed6f9bf11fc351bf40bd88abb866d4bd9abf28ccf05c13f6e9e54387e562c8361d71ee319d2c15a63cab8f231692c18e9ead07f6ef67dc2d0fb787aced22b00fab784df99ba28564837207db38ff689bdb549d4e0147884d2db6aa9e9ef172485f604fcb02b457e29d90b5a2746d372cab3fa74938e5715a7cea9d562383d2c3754b0707a3146e1a41172acc04d2ae92fec94e04dbaf0a39bcb8bd2d002f3a357d7ef008ae89713f32b2bf98be2ceaf823e5bc63b88388af80242d2dd5e5dfac28945b493bb4a1012cae3f58e08b26c1a189160ada7a77ad017846dd277e2c34a4c11587246b6a14bbb23cded515f98f3a46fe19370a3c0cea693653c89742fb6cccf07fbc7bd937a411677b41ba1dc3d69764eaa43ca41b609998a3c2c58e4a48a3b23dd991636676a8eadb6ab24ef590297e02972ac135211165bfc50462cb01291e58e22737b400a26494bf07c226703c8d1addac49510786d009a56a855b7e23ac320b56ead8df9ed8bec61847ac5ef70257b16e9f91a3c13fb5e9a4751500a72188fd0aae61942ee9151ed4b8bc15451306da67183a8ce91c2039001e302fd3cdb39ee32dc0f046830c1a7b9914b6b9e2ee773bc5c679662ad68c0cc42a7e3588b92f85cfbf517d333af542fc268aebba56562a6d3439d53348817ab7aba5973fb3049c8ec34597549cb7c922edfe6007889ab7bea67eeee9f35ee806fb91c60818a1456df816cdb8a0a7a1389aeb4d09489d5a136d5db3b332bcfbd331e6d6b0afccf8b1bdf52be3077bd3de7c077a86e8c6b0392e2661866ca1bc639e2fd313a82aad5693f9acd8d530a5732e9b300ee02c70cb3700c406aea10c12385d96f1723da07f6feb49cd18052048b932ac9c29e84d5b6a263df29e45e6481661398f67ffe1fff20ff62e4bff03fffff7f5a779e0bb75f809d532f6d3fef746f65da609c535d36a291ec572c4fcc91795c53c5100516548fd92a6ee567258540f6d199eb5432ce07ebd112dec8ae9da3c5588606953c73bc95c5d0da628ad02a170adbb4f6fb5b158a99e8c5a018c3b0d2e25a56e4a1faeb579557ee46ec9fff3483a963e99524065d4b33adf7fa2a5fc783118fbed28396557f3e4ab75cfbbe834beb7fcaa0fc4f19947f9e2cc4ffb4db8b6fe1fd9f0735bcb69f5ec9362bc8f50684bbeca8e834cc100cdaf8b6492c6281d3fcd24114d65c629d76d4e665df68a2e44b26c11bca1f6cbd6ba1641733214a8d58c79d9c7c6f578c034c350e318407fa24809072574e8676290ee29c9c6983437ae0111b76579646875e9825659530d1da40f98d690c76f46296f091dce3c2acff62e2eb1df66017a8e0bb6482ccd21a80f95ba7247a57c9ef52d5c9fc4042841ba09fb681185b4601ba5e5e2459d52413db5a18ea9ad196c0f76204f6172f3af76aebcf5d63b52d026719660f2d946444e2b5aa2e6d6cc48a448de23c46bb0487edb7258a4df865e239e99690733c63763117600ebca34ba98f9ed050d42680126e5803d96dd88d39eba61cc779125ae0073eec2f1b691fc99f05f6f0b09bb996ee4bc6722b4c635aad16155b1c6abf38c123a93cbd64cb34b5c202e34b810cb881e36c1b6779127a6fcddb335c5f8ae3fa20fc2e95e60e3c13fc7ed8701b9c8cb35c98381513ec7bba5810d954eb6754392cd2048f64e5301b8d157d32f023f890009a4a48b323abecb1e1142f96f12483b6b0981b701ff76dc80546c556165e5c1beee1ecdae3b092382fb4b8c8bb0cb9d74ce79f5f7011efe3e36e26f4a3536e5041dd9ae3fd61c4aafd21cacd27508557582703dbb9209fe9e569927b45bbe60ddf15bffb01b78ff4cfcc3e3e6da203bfadbccb89daac343184ce0842351689b2f567ae59c67e4ba1c2a7bc5e8a0ffea24ff386f00d774d01c08ea91fee58c4060331a70b96e9ad90193057f6c571d14b733c0ad05da2d38cb0d90a237fb47198c80b87908f27791c4bb32ddb67876c001587310f158ac1440b65e289ac2bb639db1e85d258c5ef628b7f756f7a207f16d0c3c3cefbd0b0a4b69e7338993b64b0b275476413d7a9246807007dd81ab8ab2483f7cb237b64caf808cc60885cd3c7402e79a736576ee0c94428a7665a2c2491a37b010e6b00f59a4ff2e1ccf31fe2ab352509bcf6c5f9a5b0e3866423cbb78b26b9e35b359400fa2e60315c217864109e8c4daa276c314c0caa931adabca889a32acaecab19f442d6ca03f10fc6de1f3533e99bfc1504c48f8bc4c9e6fd9aa3922a1c90d62edacee7fb3af3ba602b2469f588fed9c6e32b969d07ea0d930fcf1a2e3b98786cd44c2293b1f335372cb4a1a16bcb5da96e8f8519c5f55eef71f2d19537ea49652189ae73535186c276bd3c6e794ed844faa0eec548602eb3628caca0e96ab898e1db17d1dab5344b74edcf7328d81c3c665e7155a941ad4822cf7857cee93be5dcb6b28fb4913bb3c65740897728abc43d667956c6e0beb43a405abef5c35df24a1b5ed82b53f58ef6b5d73f9e34b861df4ede1dca9439bdd885f4ace4f3453a1111773292c535de0536e9ed8d9dc0645e0d8bf944ffcae703584c87c09906230090a5fe1080e7c946ca2aab46eb6d4c4c88208e32a68698bd2c1594adcf28baa4b1ec1479cea2a2b704c00e63ba3f94cb2d104f57b493aac7c996a4ad605f4805b97bb51ac1475431023f560eb2bdd8bcc42a9e4df84f8e9d4e98e8faf9bc483dce902fb3d33b40887ab1f791b88f3d7e5d0a865ed4160df1a6649e73df7deda09d47e3a525fe5368c1efe2423f09eeafd0b4e14f4a55eb40e2521716ea7204642ba9e6c5f98ab17b71264642e44f6d794516d969624da9b9548e96dafa88e2eb5a5c6e5691c308a9715caca5dd021e204376dc57c2a135d7a6e2d81efdbdb5ee13baf6a700ea3b84ed6701d6b710da9f424a9fc2687fa2f118328f7c7ecd239cf6d3f8eb2fd6e7f3ef9d60b59ba69da0b5fff9b4f0939f45f00eb1fd76aec01f7f6d83d9fec4db73a8ed7f5a4dab9f1684df47cdbd7f05043e3ce88a9c8b8041e2fba4250a73d83dd9ab788fb9b668d8c72315fd75c9c65baf664b07465ad0be689e0743e0bc37e868d96e4451a6df0446ff85dccbf42af0327d92f8d822e9a5c96384a48f860ca638b1a94d314beefb47614d1a7f9df792185a613d66487c2ba8f74da94d4e8f5bd50f52161ad21078b900a94ffa4d8b9480aa1601a567bb4e5fd9da9916c29250a546e626ed28b01da5f45e963689db47e4dbe4795f579fae745fafba9e1327997559d3b1874488b7f17c3626b5659f841f35c13a75e17dcfffbe3be7863e04dede7577f04c116d3609a93c1c9c8ab887a547a6a42a67aed42afed78915e0f98bda3babf954ebe4e545fee73b20b15bb1fe4f9ecbff67f25ceed58fff068fe5077d08bcbdeb3ac4df7aa41e25f401480a0e9920b00f157d603d4ff8291358fd92addc6972e81dcc98c2cde834091920da8ce2133910726e8e87ce3211c690a526686cb8bd53ec8d7545d48924fe7b10bd47c7e1391af7d32c31cb34b44e1f2b13feb9e3acd80413fbdb643d3b2c73f7eec0dfbeb5db5912a17f9e65c09e47939669616885609169716e27d917af4dcb28fdf30ca7e6fdd72b73f7c18b970617d3c40dbad99377146e96948e8bfe6487fd387cb5ab80d88be3f6421b02af574d4865c7f17a3818db7cd69b305c4664d2420ca822d949bb492f6543748a4f48b33797f341e992d2184726aa91cbd0de5df11a33c8c68ae24cb1cd5c4a26eaa10a4f077f1f79fb418f91fe7e05018bec497db92f057cdd017e7f65b8123f8bf8bcf775def6c6d39eb4a8c67b43f09985712c373dad473358480d845fd8f66e8ef4edf82a77723a0bf6f74f220de5464245f6cc5dd5229e8ab60c940444a43f5d0c20d5a533de9bd37cc552ede279ced9fb01e1bf61009c69bf7177beeadefd4cb15d0e26f80ef1295b4af019b2c5651353d7029cff6df77f9c6fae2be7f7dd7f5e1fbf92d06b7afe1be137f19451faa43ecf53d9bc2d3f59290af36530c7849188189b89c12c3619b0824e9c08fbee56edcfec89c1965b02775d7bccc69a14f514c1e60c1ee7044211366cc5ea13956424397523cc2fb8e938f87b934cc74d2e890deb7d3341be97fe55abfffdc9d75086c0f3ffdd27df89a50f3a17d3c362341f92c86cab86ee704a8cb8b5f2b3c97731b0b4e34cbcbab9bd117ee32bcf3c90e8bead6d97f258dccf3c9f4085c24924ed30824e90a920fbf5b4d828bddece5a8cf22563a09ce9e15b2b1b565864205a6803e66c320d9981328ea051010c92e33058298b1ec9f7fe7e5b7b5681bcc3b8f9df28c33bc9ac734ddae771d4e44b2ef98666e38468ae9a08cf0e8e7898d84b84bb955d63cf07e22edd0e86b3717ad2aa5d6eac3934280576671d4fde5814079a53060b3cda3b628fe96f5ce530642776852d38738aa6cca91ab9d470a53050e361fecc72597821e8b527dabc96e5fb41f8cafce5b65921be8db0286c44193b7bef30dc23d6be384942d83315853b6a9d724ef3c83392d8f6dab2b588175366dee9364cbddf8144b75419d198ed4faba9c745f31599ca6985cfc969518a479dc036005ad4f3c371b13bc29b11332717f0613666f7f089d67c7ae153849559b86af01e4eebdcb03f03307b3931c3f96bee242f4a93ac00edccca5bd096bdfc4ddbb08c22c9ae4186d8bdd3c8cfc14a8bc2cb818d79842bce72eb0ab7f97585c873a579cb30db50a7f097eaefded06d7082deef9a7afd1dc2ed797d30924461911ff974ba1598a0bf5967991529061bf985a568128cac982c98f6456e6b2ba32d3c4e4466968ed4049d8b432d4a35dc02a074c819982f99a7ec24cdb417e3c6eeaafddf23357daec3ff9598dd7b3de03119e605a7eb07dd0f31bb675da053458e1a86f53e3f1f713c1d8d7a8148f58b15b25f97803e9414d157f7103e3e607d2cd8f8abe11a81ebb16fcb89b5df264c2f3fe6b47394471cef436e8896f8821ffb5a6af55f4c71bdc3e47d0231fe35206d6412ffbe5735c71e8200bcd48a4c84849fff69ee6ad73f45effc9e4ffb30d28c2f02505f58d93ee8def461a4194dc86987954d98f04ce8793b20710c7c552bc7f2a00f96c864693a076528b304b05e420ee1d2a8cd6be3e546d898663165471e05e56bd7f6a51d295b6e58b90749cd0c35d09471da1699fb421fde170dbe9b084fbcd95f77f2e78ebc77673f05256eef69baada79f81a8ddf634f95af84f2b781ad92de4c75aad67192b1794edeb5ce5d4344ca4429c17e31c5dafd879b08ce0fd69670202d523b69c795a2f370b4238f1ec76e7fa2a6eec67eacc2c327ebfb4e98815a3fa28a79bd7106862cfb0ee8c35f741075ae1828155b5cc372b3abcf710f1d0851df0d9dab0d91e7bb14a8be4fb185eecc5e23f8fe49b2e7d7806621dca0135c15c359ea76317a8637f81aefd193036dd41ba9c60ea9ccc857abc9259cf0fc8e114550eab7d56cad206808cfe6e25cf73c1dbcd0f8ab587fbfb321b02664faf3c7ea1926d69cedf6d771f5f7f9ece4f2db8b7cd6ee02f1f53ceeed48bf606edebc1fdaafba481e9d9b66781ae158691f68146f7834525d503d346dfdd4377eb555aeaa1678056dc74ed731e322d3693e83defeef3b2746e607b6178b3283e1fb3f9b5804fac455fd4697c2944f99ef6db70bd7f027e8ea979723a70c699318b63d11d2d7a9abee3ab8acb2714894eade7290e9ff8332d23d432ad89f8ce93ac3d3f187f2d4bebe92b2edc7efea1d1473be431b0c64992a72b79309014bb5053761de7235c1ec2c47c55b249a0230c94b305a9e55b7eba83c8b54889331ee9b3fd60c8a98bd8ddbaa20bfb82bb204f3b0a49d4d176c4be767c4893bc38e363fff92fea7130175ef4c6d8fba1a14d99c9f3afcb77bf9872fe4ef622f04bf85ac7caf0c06ed4db4550b53f4cd0802bc4d8c2ca2ddc07d619b68b097f18a3737c541d0b9928fde3215f079503af7bd9609b8618eb1835d7a3b5d8b3020f198e6bd770b77895712f2afd9a6e856083d86467d7e820f25ff4a108f71b8fb915362735b048022bf66aeb1ac24a3dac42c60ddcff13d8d727a17ef0b3728bd7a308751fce75372ede140a147bf22b182566195a39689d8a4c330af07c18cd9f6fb48f7f14268616827a059a96ad9561d1edaf7223499f87577d6ada549fffd0e29fb66dda80cd1937fb730db4b85722ce59058d59ebea8efe71b1e40f71b659df3e09f90776f3337108bc5e8167621dfdffc922405c7282ad85d5ae585bebf48010d0283543d460675bacdf3be2ab9e1ea4ab893717034615a17a6370c89c034e83e25095e5615aeba7d05b2dc6d521902d79426a7fefff6f4761bbed956b87c0ff924fda3c2bbf87777171fc4fd1bdfff545f7de17c3df852d7c277bd954ce375d410c4d3955356e9d6cb4c1cc3fecb04826c7d39d0055a828881ab4559381adce779638c31568eb54a7dd3c4b4683d1d2e24e9009ac9dadcbacf5feaa18ee67c1508facbda52d5f3b34e94912de9d8d3fed254757bb0d8eb9d57393e8766778d85be2c2059b3a7c371bc7377d74ae4ba4995adaae82bd98eef0fc1df7bd77fb4b97f487a62699b4b6ac254fc1d218a60ff2ea6823fee89452f824d9e519ba959dc14ab368e8d4a793831ccfd0cd90a6f8d56c4193b27e340b349ce24ae4e0a3383e0a0b5c1d627d8bea5499b76d9b7f2eb497a08b9ebee24e66b73f34b5273b88eca4cee09eb09be0ceb09e48f948996a84ef6d361e7a1851d4663d77a889baacace3d12713d1c52b1daa4895d9f58f8c99fb10a368a5e016e56909248e6442d0c01cac5f347b1b796ee5c69fffdf7b55989bf16b6b79d1ece87f9e26119d2bb696a0718dad447eba9bbfbfbc1d9efd654cfc86f4b9a7acdc680673b718085696c460ed0918ef2cb1a23798ecf700401628f543586eff5bde5eddf2fc0b6bfe85b34e1b1c02120c15e9fe7603b13a3610c92562302973a4c7c112fe19670f1dfffb1ef4db1740e0dd6d777fbabb377d46a502849ea1b9340bcc8095b098da71fc5f077bbc7d506c65da7bcd49f4491a84e5f8b963c57faea87e9f22169a16371af31d96c03fad3ad50de1f6a1f5f8ba1f48be217e967a73d9d4b0ea2471bd37dc63fbd37a856dfa2230278fc35246b45591dbedc1f4ed2cbe4be6abc5e1b558df1bfa5746cf77cd32d1a95e975dfaf8cad0494b8621788a7bbe2c33e1842cc6c3e3df8eaeebc2f1e73ff06de5fe7f3a2a7b45667d95040c835a98badabf28fdd26ef846fdb2ffbd5d82f7243b680bc97cbe419c7e8aed87c3dac7e582ac0360e34273f6001f0442d50799b50b6099c365149301e4c44e75d298710e5afab8880710af08e138228de998db8f79e06016f08b1868660c9a5ad1a4c8fee7d321f8d3c188b8b10f76d8db9e26ee7f1ec2c45ddddd1f1c7b6e13f76f12f61b8a5d06b0e72cccd9b2e7c21e6c254ad5431729ef91b38348b70fe00ea3af8cbde20ae9fb851deb550df69efc6520de3fec524b1701513d48f8a266fc05bada3065b6e72cdedb3201b9eb0233f8f6d22c3cd7acff5de09b5bca57f6aef7dde06e1c7cde1784ca3685e5160be3f2b05c7350597205d025d5fee3b8d38200fae259f18de8859fb7cb27814dcfd13fb59836858d04f38a35a4387e20ae7d776445eb0d74a8c893a246c37aa9d24b75eff4e049b1ed33d924d26583f5f772499a0eb387c3a32855e4ae174dd619268aabc3bee59cf828887bf5ad3d78f55595f473d8ea734f72deeff148cd84a2992ca0b122cc6476c3caebd3ba98201843acf9b1040c0dd4999b8184b8732e96cc23a6ba3d0ad94cfaec063998c71e6cf4e200e2e20da9ef9381a276c278caf3588bdbeb342037087f3f90c11bcdb310deae9a0c1be67b2924f940572b3ca8b5b98ac581c9a08a8db91531d9eb44af2eca03b0b3b01dc9f94bb78a8b1d17ea981afa9eca9913980810925268b2bf4be52590e9e5449d53732c615f3318dc47533df16c5d594b332bb78a3bb32c7e1767750e2a3a8b4ebb00407e36413cba16dabd07cfdfdae6c679212ae2e92b6e3af3fe87c68dd32156e204aba3d10975c914b6fb2152abcc8cc9128ecae2302c4a872fa97a2b4fa51319a0997a84471e36de9f8ca4c70fa9929188231217d4610a97e1694dcf12607008433e7ff938fcdc17f6b972d9bbc09b38be4c3b7e51e5ec491f3eb1a11b5a683c7720bdb748c2248bbc0fc7ee536fc215c7e7c66edff641a0e9e58696998d6bdf8a8b8fd6e857adcd320d3d432bac8eedad28bd1402fef643928395659e695af1d7ed232b732c304c62c7bd8624346dbf689c95e1cd073f77c478b16757a09dc49fc4f794c14b7b27d34cef7bf15d5aa75aa645dd9a5e6d4cdfb48e932c3a434618eedb41a6f85a761fcd4d2f4f43adeac4ebc75f5dc1c13aff4166a55693f2568556d7bfb9a956d1a175e1455eec8076191b3ffab232f68cc4fc7ad6de36cfc2ae4d8fae575879aa19df709c64a69559e67d1720cfdb6696591a16e8c55ee169e1d743f9d2f63d51ef1b91e40727f9bac5059ae1f3906c83d7ba5d1f1dab00b5cc29af8b4cdba6f4925ad5f29a9b8de9f38fcde6f4ad324dad92f17447bb0862b2b943538ecd2b94120b4389eeac407dbcfc8a08f3dfc87cf38a678c373f7463fab8584d5948c1356a089c2cc553662b2474e7e170d305d2a86d57fcd5689227af7864fafd87aed124fb59a9169b29aaa35c4510637ab7c5820ad921e446cd4f048e4bb0b2597b969e16e931864e1c1b250eba9a39b80d70ace020d9849c709b2266c8cd485f4cb94d816ca4df8926f946e56bf8cdb5a871c05bf1574adf5f4afbe6258ff2bef9a9abc437c6f4442c54609ac8e3de89dd900acb2eb50a51c843a0ba56dff4abf59c8b68a22e8829138d127e61e78b7c3fce6bbbb7cf2469381e4f1ddf458121e78c7da8ce4314e978ba6b3bd760afd5d6cef3cba12601b16ef5b40fd95e99cd6c5b5510092d66e6e4a0f5557d31978bd80e6c2f5a9eb6738df74f42688ee13e3511d93e9eb1c8b2f25ddbdce1116267119d0a4cb914a723cc44837847b715b6e8a0f55ecc86f73642e687de9c47e3e3a3701f88bfe8d8f9da08d9112e3e5fce75645b99c760c38c1786477a2c7a8a2a7accf77d3283ec0c986cbce2304127276929d6736c85f7787ebde90be13a5a1f05c4299cde6cb28cf871b83910691af37cf0f7e11a5f1a25fff9ce30f94f7b24c6ff86e6c8ca78c8d47bf491bc60ca3ad37c1b7ce7abc63bf26df6165fcf89993b4a87d319a36fa8cd72b40c9603c24e830ef199a696bbe657492a08fec28a75a50a81ef97e099d477ccb8ab9ae4b00a980188893b4254ce1175c06d51fcf0129eb696e76f87d5342cf3275ee40749bc436fb70546fc3c04f74ab411c4f9b271037d1b744b1e24025290b13c4560c2383187c3b8b7ab4a6b3eec00db6abe69f166fb5efd5a9ae13bd5a680c6e5ba5b8261ee2c162e8f710b32dd86ee74deefc9f05af759dceda0f43d9603793414be303cefca80e0dd425152b537aead65424fc8a938d026f07ab37009b10ef423b4b48ab2b65c5ea0a4e5724bb8166e8ba3139a9953814509d27227fd600b6c97528490d66eb69fcd657a29efb6bb179d4b1ff1a42dc723d332b4c87a3b2afe6af6d907d9467ad79b2eb96708c884f3f97029f7357dcc29a505e011238d88643748bbac535613995b661ef85efbe98bc5f725d63ebde0cce4a7c74f9cd6cf66701dac847aa99a4885afe6bc0b94ac37e04bdc543a40b49b969582d6be6ccee1bf17adfd41b661ed7ad3ad4418634e9ca95c449ce32e20b8d62bbb3f85ab252ccfba32d49af70cbf54a2f242f3ca8a97374bebb7c5297334de00084a0f004a540260998f388032e6dbbaee8027fff6a6c6e0d7da2faf20e6bf53bdf072362922dfa3e3bf2d4c0c11c03cb73fb0fe4e3b04e5a13e41ea7e3d3b40a4aa6a0865d7e47826c836155b7b69bcea8b7a6c0f62e930700471ee8ff0c049748b0236e3195c4d06a3a00446d8bee872d0b95ae21dadb08e5a5bb615fa2ff59244ee683772b97b023674bf974ed833708f91264b581dc9dcd0ac2d569ada3b445e723ce6e693f920c85263a7d192cbda9e39eb79e18a84596e305e2b0abf4ff2618aa3c474e1b058a9ee8d58ca369afd9a6fc73a5986768dc6794c584adf1dbe0f182f5d4e4757ca5f44b3bc1a2ed59086c0f3ffe09950b74310b55d08714dabf10971f804c2dc6acd84893a17370283fb3c06602b4ca58bdc96f362180d56d160de1714a807d53e3dab341f09036760a5532b3d98248ac305bb3c84bf80907697eb794de27e380739d61d86da53e8462fff1637264e23302be3479cce875629687bb11686d553175dd3a441c40f41eb7401817a8e2559645e0a5a89fd59d5fda71d12d47a5266ea71fc7c7e5d675850eba6d6d4c70d88758db4f353891920be3147d6d27a5101b3221fee5d7381b95d42265b94a36625f90a84e395cde84cf463a53a836f7cbb1d9d4275384a9d2d6ad3634a7514131201898d6b0a7bae1b7f676e0993f8c3d7db12136e5ab6175b609a25a995155e6bdee16ba5a33f513fcbe4fe599742d26fab0826ec6d75eaa3e1ac3605d9a677e3dd005a03bbb2025887236d84c42d7a8a1e29d389d3692f63392be3d554c0c9b1aa1a7e1c98e25cf0c580e991eb214b03983c0db7afaddf974af281557d1c26914e126edf1b5f3b88ddd1fe24ddaa6ba2847f9ca2ca1a376231d50d9e02d6cbde72bada89ca26391eb73d5aaf47c45c244b134eacdea952c592df29bd3ee50aa5f3ffb0f71fcd8a6b5dba28dcff7e454676f994421e6e9c3a5108219030c2bb469d90f7de13a7dedf7e03010b2bd052e6debbe2c6db5a92600d31c774630ef33c5e573aa0a4bcd85b3065b54d1cef0e5810c5f7bb7abad5c37b2a8617b5707a78ea80af0510face16a987002f8a7218bac16bd28757cb40bd35eff5ab40a0e483aa59c7053936a63983a529aec3a9e267486768e551c44c05826d6c737390b053986c0b7bcab0b411e3f67c74eb0e7174ad1282bbb7d69bdd101fad766d54da4d7b3d62a435f7e67e28fe3edc95a917386a3f9f40517e94ee3bc70ee723fe6fe88b87d79cfae1e1e177fac010e6db1ca2eda8e3768d99b9b699f52e8d639fa25702cfc99461cb121471fd69108eb588eda001d58e4cb5d3a4219f45d4393ddf8a83ddda94647e8a356758d0038dd13fd60755d55f0f74ecee0d67cddf2bbd020059c13829eb21b6f167fd35e1c9ae4be5237bd29bae022f6c3716db2d26b6bd1192f5c8062ab7074e3fa678c2260973900d52b42bf88bb6092d618d1b20b63e6a39ad4da4efd6d1ef7bd44b5795123beee5a82ff9ee638796212797ec3f659e03a4562c58924f2e03eb85e5f6c2a859350eb0ad80a3fc601992d70f7a4490ed98fe761fbff6157cd81d6ec854f027fc98f0044f2ea6d24b5d7e7d4177cabfe3015710eb874aa452fcf67b320af8065ba6c276f41189ffb19df591f8f1aad5496d5ed8522eb746b59c36089271364ba563b29355ffb7cb0e2a21f1dfe0ec379f219bcfd6171fde818f3c433f5fbaebf5514b774cbd30946c3dbc4591bf9d5a0f4d2de9c2dbb7dd3f7db4f11e2131bfd399de0967e6f807805fc262967426319ff17c48527ecff137cc6287b573375c8f537cf6d0993f4ace4e3fee56994a2b0e9fcbd227cee29a8bcf8de8d33a74f3a01a65267208349bcfa30602f5ba7ea0861eaa8b01cb659a59c5d5e5496fce48756c784f2a1ae249c549e86308c56e410b7495b2123c751602dbcca879dc8cd76ac457faf9e1af52b0cdba7d72145934e1785191b674b05ca0bee7eb9e3b80db3bc5d6b6ddc36e1a1ae1ebf2814f8786f7f870054d96a587912e02a740e0c718a01c4681fb8e02b04608f024f3a4a9e355157a5008b0506d3bef663d89c5baa4dd6b4cb0b94eee583fd9558a9644c7b5d239251f969d3cebb4e54b6ed19eafbbe2c459016975cf1ce6de880d3883308c296ceb9d48f519c53e08198feeb9003358116f4207d30a955ed6769707db366067ed31d3b937b1e64b466b592264e66b9dd911e466d19fd8c32af5dc4fa0516540a535dc1d77a28f7ab97b50c095567074983eaf8213aba120fd341fc298a2adc63ba305912b2ed8b799980d0d46a47a982e4ec66d9a1d74d636b2165754268da6ee2c5ac6abdcdad8f89e319d1d9ba6fc340b57879a8060e5f897b66e59720004bca0972080bd06c77a3dddaed43bafc7689d2c8a8bd0a2234e9700fc398fa2c0646b8f481d9b6d0ecc0661701904e36cef8e486b15b54dd1c371df6d868b6e36c277fba14860734844c774d795c111bfd7c9beadc64632933211231d64a6297932c40661bd106f61b19e498b1f80391e74e8841f03853556f78bd4a3162fd7d5d6f810c3c0012f045d67833ba4982f0e7368da53d4fdaa4aacdf391aeba22997a72ed461cebd8a3db7e77453f0675408fd3757993233c7daaeb3e2f74c230b267bee00f2213654a955730c2ab3b6802f3c331f0926d2ef8529a5e3d30ec5aef91d1bf052345f8773c6a2a1c328e75c4910975deab0aa373575ef65b4a10408b374c444d99f456c39cb3c6b37ca2a61b540403c873b02d474fa0c360c1bca9c6ceba42b66fd59f0ba50f2d3ba55b4bfc84cc95e45411ef4e08a51a03be53b649d1acb8bd0a326ce972f08e35e7bba317b61c6e87e3b6c264a371ef4a84e1a6198a4c2c68437067bc89512bad15a2c8216dc0bf73b74666f957813ba2db727b50e736dcd098b09d4e2c7cdaeb1905c724b1c987aab8f1cdea2969587115cfb63396ecd6c8c5bc9852e6feeabe45f1c15aa4a48ee3153851b076d0d3398a1dfea21e1805b760d590f1cabb919627b690175364d4e8a9a0234e537fc4062d199899a56d2d8916d9e251784dd5b6c0ddaa566d318a7eae55c1fd5794abb6c5e61189e95f9c5cefa0e9ea79e36afa2cfeabc3e78815ff262ae9287cc93560247a214c710d870c80d975827a12886a9a311c9b5654bb66527baec72ff7a72bdc84ea49fa353974f2faaabe062797a43b97b13fa550fd4edfe1527c5de3c7811e32a516eb2d090a9beefea6410aed67396eddae276a2ea7dba1c4fe4d379dcb579ddb940d3fed934b03bd1a766df3ca80a456e3846ae8f71799eef2d78d0d8b466e06134eea02d483be84d393c7478727560d68ebd5b53a4e08cb2f5c4516268a0f6c8a42b8c1a044407d9d8427ab8e50d07445f46b86dc9fc7c54cdeb91f1072c8d8f23a292c5412e866463b9f0c61ecd80032eeb4e02886bf1636eb96f694bc41b23fd593305edcddc36b51eb5a059d3cc0f07743b0bc9358a925d2ded9b01a58c73634d35e50eccd36ba4d239c9b535de91acd21153775bbc883da9e47c53656b8480a63d4fb69602a7849f0da6c28894a69d602a91ecfe75c2d6b7579e3243e90ac0f67a8010f53451083de9a1b8040a499f07c54895e0adc88213a3973688884e9461c61dbabbe13e94c408b3c4c30cdef3b35d8b320e84a77428a42f78ad7cc16ea240661830a1acf1dace521ef706b694313b358aa87a66e8d366f5f31ea2b9ba8aa3226a5d9ade5b6f793a092d547cba04d06a4b52b41a6f83ccea658b0dbe827b5b5d1ff617e45adaa9d1bae55b8351a30b3597e1069daee63b50171b329f87cec24c49d4ec80366975f31c648614db1889482a6c647e1bc5b523e2ae607c34620b22c73f0ce777117a54e0f9b20a601f04049c00aa4c2a6276b46dca09266fe530ec8e9b8756afc2d273c349f93a70550765ef4bea57634e28ed95b0f49ac4a11527703ace1718640e350a1a86d29655b1cd0c8fdac8dc91fd79b61a38a1b95d47d66e678efc7153f517d16c8f7846934054bc4518d3ee20f0274af7c0a7a39d86d59b71af98cabe057a1ec8bcc40bd61d4be7339fc2c9a11e6abaf2d1592af3610e84115f4050fe39d7c3552c08dcdc54733ef4425318331b4349637f0ce32b2d0e7a09b6343054fd3c006551048c5028f30e366b39fa2f428f8d395f1673e9e3d99873398fe4d6d42ad75069bc362368b1d4dc056f8f5f3bfc3f8c9f73c38e079017ae83e0d677709f7e7aaf221950f4a0b4eea91ef6d245e85145e7cb6a384b9b59a6d19b708826cb945d8bb9bbd6f37d906dc22a6525f25bf6b11aad28a221f29975accae292e568078c522598771d5353cc562fa0748cf2a54ed8f1e57ea645686676574198689a309b8444af811aeb66774234264366d7e4073d026e6c133f2299ed8142a0bd5096d976df724b16a3c07580c805442d706d3d2e8fd721bf20e8fb3ef157af386ae7c563e0fc8ecf0aa3e48635ed643dd1cb127fbaccf79d6d6f34f1e6fb6d879c674a231e81c12237c368a61d0c906c5a108435fb602f63986c2842b8c5b4da03f5d082d82639e888c2d21fa2ed2a604537a44fafb961ead4c85d84166a395d0285a40aa783703b3a60a1bf6d710331d488ae4f8e0f51beedccedfe0c196da651df60759cd2d9818f75413fd1db5384f3d5301d2e282512e25683711970462d3b36acd0ceacbdd82f6a021cdf840ad027be90e04ae8f2b01b697ca85dd99b1e76a1820b490a04b5ce3ef73eec57f6c53391cb3d147fd92268bb86fe169f8da81548ba917b1c14d73b80a81642eaa6e468de6366c428d9d1647393cf9354f3b109e64f35119f8e0d0a1776aa4a5b0cb6cb29d56a3493ccc7d60361ccd1fe4825346573b0e3d612db433357d1ec25a33151950375f15b43e04d456cbd63e38ddc8b464e77d50e8e4bde9b3842676a9336430d1c7cbe0dac907595265a21662e3ba22bc9274c963f5744f925f5d89ecb75b522ca600936f33ed16d76cd7c04b9dd14b76d32b4842954a935f7e6e3eb16d55ac76e2517adbab907a06a2b1a343683fd24d92c875d5e5705a7b18130d9f26192044730ecf8c162875143cb45b67823112dd66e910226195b96b47b5b5c59208d43570369744d0eb13d3cccd8080de19a59de57aedf526638d9d1784794a533bc5a390249ad41ff20bcd0e9fda3a266b5c271960689c4cc97512e8296c5314ecbdd889648c2d8509af3fa94f4f63b4a6507aa06ae0691a72e610f962d69a646336714ad12653c9c0e3361cecfc6a0363093d12082d64ebdf38b1af0a2acc416a094c010d9b2ed06f9e5e3e6e36e12f1de038a5d49cf7cb98cff5cb2ce4568d113a7cb2a493b4548191eedc73b0ba20f31b15b9043b53177e79c6c791c3f09c4bedd6b77f6b120534ad0ede93cda42dc0d3a66d0754e4bd87a3260d41948f836c434d3d56008c9ace3c0395cc59127074129bc62b3166d6121f1a882e3dfe20c5385aa5009e624b539301b5d517d3c620dbca99a9d341a4d043c9c4d2d910e169c62895388a76d3b888733389d3b07196cad7153cd0fc400a1e9d6c43b2444cbde13dbd148b1ea8d402f08ce8184d2d0a81c046e00bc815d45ea8c9eb3d093ea8acbc2e6ad307a08891e4c6c5fa617bd08de936137d0dd60e838c81ef68c8622b54d9bc167ee264541901a3574b1472a627345f86c570ffd60190e97468a2a133a87bd4d9bf31711d5426b069e43a048e2d4cf7444cd0297fc8d164fa7f627a0eddf8f353f8bffd2ecedc3aa45303d4899c6d19c921803d45caddf5c794bcce6f3eda49b6da76da9113a191d4e57ed6d73ab8a2217dbc380e71123a6c6e97295eb44838cb186ddedf3aaa1a2cc4e9e089c542fec5afcfc2bf9d429dfecb58a438017c282cfa97ca543ea6c3e57c147ad5eef8093c00ad043d45e51c1dc9c64b3751a7b60ab07917b6d7da015320bdb137f7ce88b09c306be8a993088ed7958d8b868531ac22ce4a92347dd136263a1692d2bdd45a37e1ea4d338aae9330b8f074d2fd06d3dd2932b9efe3d11c519d50f10f4afc30af47458f9f9ffbc2237d50b5c4deb666382ee69310b04d64b59ef1369e6770aca9edbf2e7e2790fb24f1d7ffba46a44cfdf0739c9b1c61a04477c365aaef15517043772eac8243212b7dca83d99361aee61b8de860cc37836088b10b2a274aacd7ab0bcd442594cdb36624c7d2a5de6ae204a42edf5ea63d714902c374565cf04c42110e6b6e05e691c4b7b47e43d19d022fbdd71a1c691f02ab7e895afbb6ab5a51dcbf0b1c97496b2d2041569afcf1029396f4afd5d1577e8e96d27a8ccd3302e8bd340b5d8715fbde0dacabbc745733ff0e542401f1e74d67a7fe64f403830e2fc90329d5c1b8fb4a802b2831c5a7a29a6085677353dca2cda74bc00b08a6ba8e353d86636d64351df77b1de60efa6b2311ace8c4d3fa61798483495cc5f0c2d871af71aeea17db07bade1d2da36acc1426d80581a76415369cfdaf2d8ecb6fc05e28e61a5de3cfacf8217117ce0457c8219e68de4e7a93c077be07fbea1887b627a7daec4c7eec28867e49f1790a837c95b2f249fd57da5217cca523d7fe3d6dff3c0b77ffe46a2877ae406f78bf48328ef0c6ff78cd62d877e2cdf70263fbce131cbea765bd22d19909d28c86fa09f9fe23597cd8bb780409600c13ada3f17b4bde737de54d83c9cbe8e9ff017553c918de9aa73e1613a6a12bffbaca0930494403e5b864f7552ba1d07761c7c51fe369f9662a71879c1cf97055e4608e4bc6d9d6543f0e3a7ae73b4fbce912ddd51752507523dd2dc38028e9f5e0a829eb461c9c98541077924e6bca7ddc49efd8691786ded5d636c53d2832b7cdedd1b1d3e8a03de2aa084f8e0d6cd706b1578458f5e6a039bbf5a0f90b945315b08e84ea84bb7a5feb7df095c3590c3327ae2624df55efb4f1fb8baa187f0a0ee01bc13eaaf67d5e9f3a24bee30bf9f5e127dedcbd8e34f8fe42c02ae9f9ff2072f9beecdfa5596f6c58b6ef08ebfa87e615d2119044e7fab96d51587e00d481be30693acda5d953c181cb262a6dd1eece5bd7415bab4d558f725f1c04d7b52a6f469231e3a39a84e30d4e6e22d31cb6ca13d57dbe3ae03bb437db192370dcf08de20a17c3dbd2df63dfd6a23ccdee8e6913eff9bba394affd28f116645e903544d470396e82a9341a0b0236bb23c30bdd5d24091d164d3377d6f3eca6c6467742c916a76bab964b6b7b973d8855d7290c55143f7a306c4935b336bafa6600aeefd7100215be6f0a6c0f7b58e9cf0d9b1ffa8a4c751ff0d257d893f6ae9ebe6057072a99a7412a324be15812bdf9b32206384b2b18d3183eb53c1c141a194cd2d239d0e706cdd6504136f40e48166c66c9f4e979360b75d42dd806a90a805b557049feab3566012df1d4a5f3bdebb7154136ea8105d584cc78b620455540d34f6564a1a4a8bf678ec4e88fdd6ef046db9bd1a4aeb01252601ebe51961dbd0269e591998abbda037230faa8e4d34cc27078cbb4b652fd0a2b1b9ec0aedbe267618f18ee9e1c76bc3e947a9f1f4e3763dba1471ffff5f7c7c9e96cf06c48fcf96c8cd774baafc2f9bf7bb018dd7eaad936810385f140319afd65b624e53c3c017c19d246e684767b50383c28dce61d15027a9bd3a700b5a9c7767bb345fc42449e6e0d088b3cd680fd283de001fbb3e34d10f638edce44b31697128b767c6df9def0fb6cb9f873fba7dc1514d37b75521900aec49cee9b423658cac525d0c1b0b9e1c7527f97ebf17860d74ba8c9bdc741f8dac31132410d19348b70b618bf6d0698926db5eee97f95040f274d2c3722897a92ceb770867f9fb90136740ee1395f74be3f856578012b8f68dddf071e05ecd94b28e79b42baa77cc59f61701a457b80b2bee5556d21ce38da549ad1dbd83f014d4539bd4da5356a0e0ba8b98e8c909b2a438d0de0f07a4b236d46e7fb9462354883dafb7e8eda8c8ec043b9acb96ec26c11758b7a1f393ef2ec20f5afdf3bbd5ed0b0a3d5d6fbfb363790238ded9db30ea987938a7b396a74f1828ead39319437790c4ec217d1fedc51d48a27175e0818d44ddfb61b7cff25e92b726c8ac81ba049d267e3c6545674fcc7739fadd891e5aba285f0cd7f2d154d732bc8a07819b9b1751ea5235cde2c6ba8bf4d2ddc892863ca474b3384e56a364c172c39e6ab63dbd25ede5ee7a482c7d52594d960334a44d13355ca3c52beb85d2d9a9acbbe0e4c8c8dab30dc762f6cefdfd295e34a3a05609cfc79d1764a37c1805bcf5e0e97c857da2c49695ea52a401853fc1734f1e9757200cafbbf1e4132a44fc25ebf5ed0b8e5d7973fb9df53a085add55460a09b7a75689885913866d705dc899aebd4363a1a7ed3519ad0cb22de0c65ed6f6233a6ab2e3eefeb0d0e80113322d51d0ec662f9c30ca861dd36b448c91deef431fde27ae3c534155eca6877f78381462bf83a77127eb1daac62b43e97bddec7dcdd7af9b126bb3b49b5731a1cef23d43b084ea60fec881261ed26f27e2aebf5cfb64c3a29779e4506cd24224a9b58461be3f4031688a18b1bc84c1996c330e4b29d980c496928c7107b50f718fddfca6abdf77f78fa713cd8b9df7c703ca4729e6c78bc37bb9f50fd7aca72a2483c0e92f50c8a936e126f03ca43ae3841a25bb941653619ad3c8da5e2f16664c37fa90dc68e98e468ea28197a4db7e8acee6d3de5ed3b5d944ee4d9a3a147b38a8e09cd065b713c99af791dd3aff03abe79757b4fd3cd11e9d4cd0d337ee76ad6737e78f174be243adf88f0ac02de7e385e5f2d2dbaa9f1a91da5bc9572ff8e9be2a4b67d0696ff56d8fd3add16eca318c93ac07d802d4764abe9ce619afe2d45e73679c148f1bc8d254b54e6f86ac459a6bad9d2dcaa2be0aae18f2e01130cf296096ec6ccf0eebf9c2cf3f5c096f5cb24feee0f397ee3309e11761bf37484697af7cb94c9fa25777d853786924fea470cf8a55dd0192b82c6c88d54b547a947eede2eb3300ab96aad425f82411fa23a1cd42d3bcafd3ae3515fb73d4325662431adad89e9582d5709b6ee96c30c8d085cfd973066e77423adf1053a1db171d2e612cde2726ec021f500c6f36ea75f3f1575fda71655047efa27fff7644fedb11f9173a223f7ad81e8159ea7bd89e53094b55c360642eee3738dd5d33b1d04696f27a0785446268d37677878bdbeec161e68aaeab84334ff4e17e27cd1d030c07bd4d17ef109b5e7b4e6d38da6f37082edc2c7687969fc0bf6fd6fee31eb6570bc6bbf3783de3e8e92d20f0fcac389957349a3a2b7916ee240e6f10b36d57b6c3c626e95bc1564288e920010fe27cb698493b2893b8712ad19d69cf199361421ca689d95c4efbb1b5358720e681cc520d0c7d471226c2ff3ea6e66dd4f785c1f331b65ba1479fbefc15c12d79657914f7478929f77d03ecd2a2d799c4b54ae96e045ff7e6e2b60a5963e154efe3ab25a513ac08f54674eaccb1d6608d2ac336371ba20b7cd26e610a84e399bfcd3c63857b5646330b968749be3decce629a6fcdbb8e015a0b9a15062eb1a229d259d5c3d090c34016e3207cdd513fe5300af8a3caae9fbfb78aded776d729febf157cd5f7a5c6bb52e1ff9ed8190e6349db06bc1d8b4640ec4db517720628cc7bdc648d37fd9d4c828231eef8bebf470fd930df6c1b24b732257b6e77da81ae33d375e262590bb731d94a74ce6c562a0a7b3961fe6059e7b3fcab8a6e9f5629f42c72cd0e36b8cd1821b3417a900f1014da0782335e797203235d3192db9969d3e3c3746cccf9fd58dca66963ec915e4427d1aa41b447080e3bbe3ed264b39b75336acaafc665a4ed0f9a7ad89b1f191ebf8fabf7b82717523eab20ef88cc2c8becb14e82c60881a543cf83658794752924896c331d4df4146118d35b4b21346d68844fb77729eb43f96a72f0606eb8553b5e10a2340ee91a8eaf429782ea4dceeb1e8cfdc21e5283eef6df375066c7c6eb36ff6609ac33250b9967d5ea365f953e54eec7349d122b4fe21b2aea356683c4d934e7f9720b2ed6324b526350ed46e3ce9c6b8aa378090fc4ded0594e13cd58e37c5ff6832e0986c2c4f59ac3f060378d90dfacd04ab5545f9b4dd9e4aba3854266a185e2aa0ae3d3510b0b5bcff7d881d90cdaeb7163315fcde5d1029bb578a29f457329eb91ebbd67b75b013b261642a3d3a6d65d47da2aa64caf2444c82539501b3d813215474e946e9a3b1b7e5277f97f58dfdf8ca3eb4651862a51478767a98516cfd705ae44053de2e89c241b0d85e13adb698a4cdb62de6afbcad84af6bdeddef47dd6347c7fcb248729261fe67b63d3b0a61b2345b0d516447ada6a4aef0415ebcd5176a75229da6c1220559385e8799f2cd5e3cd37ff1c3cc7556ca1c9cb4d15880e08a0641424968316d4b73483139b0cebb4fa893b82b37195a9f56ecfaf598d105eb7fbd34e5f09f9ae1bb4a783a127e4591b7776c15890bda9ef93a380adc0ef28477c192122f4ab55676c47bc7a6c41c4ab4021e1d3cf9ff1688cc3dbc4d3593708405fc5b3b6bcb0852e2356f8f9897c3cafc4421897417fa2bf6aa5dbde083e36e7e616409fc0b35f4ed403c1e496eb06db6428d0599bf474196e24637638cc206500f52658ae2f15afd5d547dcd28b16bb1c525b0334a7bcbed593426d044d499d8ec881ecd1b038097db927d395ac8ae38f956d3d8ae40029352e6ab9e96e255fd472b907906aee393d6112cdf3c15d335e18e280123946140d7b67f447e2b4d91ed3fd1e91879dd4ec24e656f1f656b7ddc2bc010cc63a8e5243fe202c37206a4e90dc770fabccda1e185cacb41d266f48fdeb71199c445e341156613428e00744a4050de80d7be871daac21b53c73e23a8d8660b6355c943a6ec740f51e3e43733c5397877cb1d95a43a78981cbf5c4331c65eeebe0b6d9f3d81eeb6a5363c220076f5d5d0505eff11baba04641df55ee97324eecca85bccf1a41470e443492f5b269b6f88ddf884081c6f8405c203d6fd95677d106c77b535e940cd0eed072727042277707a3690e939d962a30339919b8d330496d85542245ec83609909fe615773035dd51dde7a76dbfc77992e01cfb57245b7ca2a369abfda3769513594faf58207ed5e1e03c51b3e42da70d45c037bf370a3e446dbc7faccac85b5d55d6bf91a33fda1b9deff29a09823d794dfd994354a53ee44174dbc7d50a540e538864005eebbe8d2559d01384cbacc624a70ab70e0b4306dd97656669675da646f93d14e8333da6e6b3f19d920dad4244b5d8cf298d3ed30580e47ab71cf0d6672e681fc7426d7b3306d09bb425220f7f196677cdd72f7c20319e31f383bdf7130563c1ff3929089f9668de2be3ec89385873597c3a88945241a0d918168d304e8644a47d21c376b21a1bca484d4e1fd218786db794e04eea0c3d03e3b05e729d8897bc2168746f514fb8972f1916ef129affc3dd5e2679ac50f148b1fe915df512b3e76bec73b12200405c074b9a959874cfc41763120ee9e1421d58fcc0211224268b6e5c6531e26400ae5d2d9baafb33d4baf07e4fae5ae857f21f7b53fcf2478a77cfebb720a398b644702428db7ac827dfd45b980e7867a06881a1ff06224078068f161285f9002a017d50d80e396c0be840eef4901afba97da8fd64369827b932ff16d9ab947b2f2e79ec77fb56bb9ffef49cb0b39d5dcfc4297e85981c4f2ac33153ad306d473a35de2ef430191450b4b759c54749c64972b71e2a61e2e26a1a7368dedc1992f879461833da223a934b9963bd3f578220efbe160fcfb79d12712f38a895fe5748a8f0a7e1c5fdf51f0275ac542761538df140ae5c6dc08a7ac39b5ad84961be430e032519deffe00e3d8037958f3f6045faec2a779f6e7936aef5f51ac4fb70faaa6d842c08a6fb777ab60ef2bbb19aec5498ce9129f651b7dd6fa03fa3bfdaa9b0aa34afab31f853deaac5e38d63eaee3f6c96b5089a56badea71a0a49dc44ebd8ed456ec3e8d1a32b5b65a6a5da86839f3cec56225283678bdddea28f4b44b1daf8093a00ab8d010bc8a7301a5c78104fab38e8dc7dd41c79c465c8ba69673764f521246c1436e04cb23fe00e5ab89b422c109c7e47e535fa13e2ad27c10aeba814353535274cc30e6eaa582f0a2287bd105801cb9dfb94ea4668ac54791fc02fef0c78f9f822be5373924d02fa8f5501deb3a91ec4480a487c73d2efa5ac4b0fb94a1cbf76eb0d21fca0e45d73575f9f4cf0f95aca78f80c2c889e253d921f458c6f9b58d3fee5017eeab1302fbddafba85c83afda4fb8fef8ae24f5fb86bfdd98f75fce461132eacb52b10f5b366bfca4d9f5475e2cebfae91e1b3dbbcf856a4b952f8ba5dae733417f5503b0df27f3d52b8fd2cbaf4da6ee44581e6c58cf07e3e01ee9cea33b31ce0252938bfe0a137fde297e18f6d0b7847956f06d4bf9ed02c1ece29d863d342f90472d13ccec5874f824406c2883f011d9e00441ebe1179811bb9c74178b2849f6948223e8ac32fc4fe470ca7dc9301fdf221fe0bba1b8127ef69d17d2fbb2ce14f21f8737ffddb28fba78db2ff4fef88d169a294791d6b040a0a9157a308a9064fa4187edcc5da224574f30d1e0793edb04993b162ee4557eff75bcd74194f63638bb59b22892d23cb819b3d7845a20b70d399af7d6dab287b0b1e4dd445439d495386d16db012e86999bdf8a754516e2756500d04b0bec807a948704aa8b199e8ccb5b1c8a6b3ae21bcc636f9b4d317d9b867b89712a48767db11aa41437e27a29c93a4ae117ef78613e5f2f5fe45dcb774e4f181b31d674b48d3dca1db36a1d15eb2ad49ce4a14a5e470d254b6066dd9ae8936f4566c81f8c612d6aadc16b4318172de643a9b8efbaba54b4587c670cb4353476633eef797283d043c8bd79d5be01ef81629eb479594ab2c9283c2e523e9f7dcd78fa3bb8663f45ef67978df3c29c67705d7a8e010d0a0ddf0372e371be67bca85319b1bafbd85bd9d739d70c9aed7ebdd210822d74224346f74c2a1a1a04c6ac1b306132f9b26bd9841be0be1f82a66527fe71376dfa9e9c1d3f84092a3f379e9111d44749d04b0f4e8729e427fc177d64c645ff0309abf10e45bd3e55674f991ffe185df982c5ff28f33e5eb063889ac364f1264bc75f61d3171f2d45cae7a7bd7663a6264c9c47cbb9565899b23509a5ac4dacadd2d87cef4361f240bd999cc368e0c2dc579aa7547d0201d313024f47cc9335a60f0fbf3e411d6fd7fffc78fa2e2e2c7fffa817c73aabce568acc3237a96799a1a6786c64afca1135bc0675bb3370a633280d12ebb3075cbc55783812ac8e6105be97b55e9e1ab0185f40498d87571976a089ab86c442c2e5a2dc90e556996391b4b0cf29619920c39f2ea81f57d515afbf165dc3fd8cbafdc9fcfe8414f0ee353a10ffa5d1fa912f0aa7d3c235e6b539a77892effbc6bb4aa0fef3949f7cff9f09ea3272f071b04481e8e0787f6708707cb00c327e35e2e248e0d86dc5fe0c3ab9868fe6f1fde8fdff2e1e921c08ba21c866e70df037f7e24be7ed5c91e7bf141d571795c046d4c73064b535c8753c5cf90ced0caa388990a04dbd8e6e62061a730d916f694616923c6edf9e8d61de2e85a2504776fad37bb213e5aeddaa8b49bf67ac4486beecdfd50fcfd6247532f90bf6f227c55fa43e223fe6fe88b87d79cfae1e1e177fac010e6db1ca2eda8e3768d99b9b699f52e8d639fa25702cfc99461cb121471fd69108eb588eda001d58e4cb5d3a4219f45d4393ddf8a83ddda94647e8a356758d0038dd13fd60755d50fd756ffa3e6ef950e57533a2ceb21b6f167fd35e1c9ae4be5237bd29bae022f6c3716db2d26b6bd1192f5c8062ab7074e3fa678c2260973900d52b42bf88bb6092d618d1b20b63e6a39ad4da4efd6d1ef17ac94ae2ae54008cfa3bee4bb8f1d0a7fd3740bbd407722e54d99439d74848bd8d3f670be294a1c3e26705a5ebf37e3c6bdbee3914b345ba0ad75133fa48d81bafaec9350f830022459f6de525ec3b572a41f6483c0e393175bdf8bf6c9e94a33a4c6664fb4c1499a20a0301ad10a32f3cd0a2c0dc51b3f30a27f3f25e94beaa54d5f9ce81f928f0a96223219f37b1132db74632c6f3663b9e58df09d15d187fd4e64c3dd8aa05036a73af8b4231ef86d92f9b3197868d8144d80e8386d4294bc121b3ad25d0e79c3313b13738dd6b3b8ffd30e6ccb11c140e625490f800b2bc2d91e7eb075ffd37125d9d20550097f85117f31795fe05e02271ca3b3550b3df97cbe50339ff2520a6735fcf3822b7d17dcb07531706f7122a15f5049d248d1292f812cdf8cf0efcfdbf2d75c06c66b30cd6747ec8b710fcd67213524ec70ae2b5a9b7376f35e7f890a4c0cbece2a7956802527b2136a91ac9731b0c17741ab6f35fb46f8a5b1378f8a267eccc49bee3c71efaa38ed8fc77e7fd6d0e6ad7dec4b983caf90f47c7c6511b8f9a3cc145f52cf8d2aaeab7253a02d643b1a65cb35936e86c86c198cfb737fa5ac3c970bc1a626cc020b760f28dbf43aa28d7410216ea169b6a4fdbe6fb20739ed8ea4710f6965ec88ed6464875466cb565825d759e1731948652174df30fd377f41f506f9adec422db70f8093d88fcc41a69918b3a51225faac0391a0102539341b3859528b72f6fa7a29d0938b3fe6040ffb7a45d0553507bc589274a72cd51fb94367adac9f5bc947f5dcde17856715ca5992c9306307cbde70b2d40c2590d874201242908c71792d273187394b03970fadfd0cf285ed32e9475a2cce696281260a2c0604345b3b4ba2b946d0a4b980a87c16467ab30aab89a2ab7150caee5273593cc93ce9e278556dc1e382bedef477a10e46545fcd573962e1a316eae49b5a69742568eb27b3102b1b254f18d17f522df7c20bfddc3faaa6a8141bee290a73e7e874c78d0c66616e776e4fa355f83529ec074529161fdde1afc02fa1539ed055ca35f816f906a9674c5ee55ef476c68541aa15dea1e184e1b66a3400db9bbd8f4e124b59219ce760b9b30a7d325d8cf2718321565c2af5757549ee94f57ad64b65d00ef7932d68a05e975d326308a5c314c717890bb9eba09ed9f519d0261435d9e64bc9f32af8061f24fc7917c0ed0b40e0eef63bc77e1d26c6ba880edd640c5a4e3fcc5ba936ef0e349158eb22cb1b1d6319ec54081ab60fca7eebec98083531ba61eecdd470026d7818e9223818ad5003826c233cc07d90c07eff047ac5997a815ec01b0950d0e3bc4140387ec794f3d40da4f0c60efec659f338c643fd505e175d8753f522f43c878e9700528d5d95181a73284970dfd1457e3ad6215d5f8a4c0bebe6bb74ce81d305dd8df145e28f5ac604de1811386176b3251dd121a12ca8ee8e0ba395d9db30b33ebd52b1dd7ea6189658256e5e547614893a7f14cbe12af6a48cf34d151c070880560b72b01a4e6d2eec7b06d2eb446c674a0dc7dcf4b5e5fa69afaa108ed043c0896de15c2ef194501fc89ecc47e7edee3c2ef1d7d1862f653e3213575857feed30fff17d8779c9fcbecbc82b3bc2d499e357c1c5d0bede5661813ecef51dd45f512ed2960f283fe8a4c4380f9d1585b187ddb6b1e119a3c56084c9217c6fba84147a3e42c380b00d596f13b4eaa45a27cf0ff39d3ddbf3cb810037daf11e92a63529a7cad3287f3759f2379312df67e6c58ea7df64967e6b8efd3bbbeedfd9753f5e6c883fdead25d2f90421e9efc801eb6c93b7a24febc9cd832a2796e3001a51ab1dbe86b62126d3a1b95aef2226e523d788f0463bebb6bb9a6399a0b1e3b8c441e80ead2cf88d2465bc46f5f3c9406b0ab034def2f86e03dbcdc86c2da535bf1d5835536f5cdb769d939e5e90d8d8bc79d1e28b88be67aa5f1fbe415e29941497b17bd5abb23ecbbc7441ec55adb386720a9fdb2306da924a16ab09374f2dd1c415028b76a3863e6f1a7e1a7201978df7797fe7af624a639b16dad94a1ca46f417a19b645b233ee9990ba59f432c85c08794dca50cb15f948bed615be53e1edb1b42cd6f2fd4c99abd8a322bf6e8a584b857c99759a93c8c8547951d14d38d1fc391a277090c8826f53091da3863501bbd0fcb071d99edb6288033a439ba8abfbbbaee9a390ede8adde61e6d86d4cb657ed5016cdb4e6ce28ea81185b7c503899bf1266eee9a93e71045f514fff75dc0dee3e4b836b025a2935a962c5a106145ffdc4855dcb9679125ff4dae3c3aa760db247b71abd47da6d3866fce696f3857627905d2796173b4febe1ee4ef022ac81c44d9cdd9896104ecd4982ec87e6b44d1ec0a0018d375d7eec2e66706bdac21ad04e24887a4e8027ce7de4de9ae7a54bd0e052a57bb44ff0925e700bcb3890253d90c5d22a59a8de99f141f8b1071e1e0150b5336404b35d433864336f93085a142216be1840d4f0b008b216df5884fc5a41355ce8a326b75dc5d3dd0234557814a0ea6c2110b26cf3039f35b9a54643643f37ac95c941fd7af8065f76e57f20df3d037db2cf1e435e75edb367249152c572dbd6d69f1220c4b745dfcc76299c6fc310c214189f0c37a4476d6752138c87e070ea4b332d13a67cbb8d6acec1107bf0da9f795468291846c088389caeb8fec21eef90df77a3fcdb3ebbcccf00280dc241b552fe4f228bb978bc7891f5f1a269ea8c9f0cd73e398bd361960b7d54ef8cc7d328daba9d0a7e18379013390078552ea5f56cd65c616e249f9a74bd079a9fd7160868b44485eaf523fca060022de14d19eac25338ccbdd73ef1c796d9450649a9e58cd4485bff927a6ad1e91a286455b0d9acd19084c731c2f7a7d00c84e52e97359589301fcbc4564acd28e58258537c5f6a060cbb492728a5357a88164c1716d5dd3a8b39b18af6d0046905ebadddc01be39ec6d65b29f930774453bfe4aa3f1c7445d716744796eef1241e6a216ddd968b52cb6b3a6e192db3e206291f487259ad50b35e58f722f5d417a7eb8272ffe39cb1c74cdbcf6868bf8dc7e476ea1ec2c3be9f8299c154e0c07dca217edda43af079f7a28fedba7b50d4d67f861df328bce9b496389b38ad83bccf828cef062048cd5f27d77c182a36ef3d644b97f5f20341d8bd3ab03a3d7c66043b518115323ea71fe4113c83b7fb8d83ab909f35b845ce69e486e4aba41f04ae0dbf592f6a9cef8e128b16b8365cac131fddd03a18ad3b01627a934908cab84a828721232986eab7fe884d0a7db649cbcf6f858b34285b53897a61c0b3d0a39ace9700512d00b86329becbee7b0e698b420a214a2275c6ed4e3ae1d8943b043dd4f3c73ee9630eadf7fcedca1fa4d610251b88b74a717692288b69b46d62193d88763908397a1392fbdd7ab6ffc763d9f144a7e85609d572ece84745f2d68588b7590e39a984f707253e72eda21cb9140bb5c6c07df98e5317bdf8e05432f7716c4703049ab0d3c49c23c8fa3070ba1e1275768e301bd75a9a3eea5c57e4e08aff71874bf0916ff9c5d4807efcdffffba36cf50b7f05326f9dbd207f0ed6ea466ea1fdafbb2a10571040cf1611bb18af8da1d7819baea3b5427236b757b8f97a357968d35b60c1e346fb7d40bd8bd0a2356770c142d2e7e93ecd6c3c64e0b14c52c45ca22078be0b7d7eda74c67a9ab5e43db9edf2f8011e35ecf9c086a95dc0ed525db335c33325633548675a686d66c6dceba9131fdd26a4cd77e24fd3fdcc346d551b920eef9c17d1f6fd50740a92b04006d4fc52e7077d3386c00b4250fcbef2c83ff4189f136247b28a3e793ae2959cfc1edafbf25857890218aef96b5ebfd10b5c817fdbf647cebdbfaaed810ca4321069722003f95d66deabfec0fef06faa784a97644b56f9487e93ce50b6f7e39f8ff5026f1549c5c02575f71bb928f5c780705cee81a216f02cffed44f8ed9950aedda7f697a858741d918f009b3f039cdca458952b57d4dcd4795f64f2374df32bb121c07f1ae77fac97ef74565ecdfc9489f05b6f0c5d4b0644d78902d73ae75bbc4fb1faa30d0ee422c9ea84faf28dd2a2bfa6d76b84aaffae55ed9b7078b2073c61943cb6a6f90bff7bf68ddb15f94f2f97357e4c248b1160e982f8e1e7207fc7cf5142c0d61ddde3c3f7da79422dfcdb06def5d75daa683e8ebfd72782573afe5bba5ce563f51d40c4d10247ff19ed7e1977a76d1479b18d962cd025fbaec68740ece8c7fdeb672925efab1ce857dca11f606f4f5f7a24187dc980f540028cbcfa4eaa4b32c05b979f0455abd97e28877bec5be217f4c72772c5bebd9f0565c59e8e62e9aa76455c409fbf7175633d91d71d67e76d11d98bff771df1920a8f3cbfbf384feb21c00ba16bc5d12d69e947d5df8fb56fa44efe35d3bc3618ccdf35206a40aedc364f758ee652ca5be6db68ed9fd7f637768ac7b1f8b955d7f1ff6e9fc0ff9926bd9f3b69c07b5ee1a5a9b027de4ce37733a5ce5a55f2be478ef4c76ca33f6d50bcfe19a574f17fa95950dea5a7acf3e392777158555cedf4b080f4f99f60d0dcceb4770bc16faf736ffc004f1e89d38e4d54c88c38fe7a3d7c37068e87dcd69f9a0915ec6bf84f4c87f70bff233bc4d3569ef39675d2ca7b92f687961d2cfd9dfdf358fff63ffce4609b921ebc777f60bfdf9e0f7bd8b9174e83f0f34ffe66a2cf5fb32438b22cbd458c3c76c03f64f7dc5240bc18f82ff0019f8cd890cf7e5eca392b58150f2187f2c104fdf9637e65a5dcfa219eab267fdc4d87a35a1e07fe6daf3f035c9cf5e0453faf35ae8f9f7a36e0f1a2f9b5ec1c1770fcc5b72c57fd92f2f41b03f1ab6788a7cfbe1272ff0b7ea8df2a3e0f653bf95a135f5858117f26d7afd2e5debb1de559017fdf19ff12bcfaf9d47e37949de42608fbb9959e0d5c7ff73b43e46fb1ee1e46d0df19a1aaa8fcfb535389e7e15eab0fb0bd6f3be33433ca07dde372f7f70dbae758dd33927df1cd372ea67f3d7bfb6fdc693fff05ff229e5d56a11c0182e58ae6b972f35f159dd68f86f9bbc1fdc702208f9eb0773df9b73831ce27cef2c6a37fe5e1e85b275a370434d796ef4bb45ef5d6df12bb714320b2bdff313fe6b4aebf313cfe297ffe5daf9538256f7559f97cfcc293f8a7a7f0b7fbc10b5c510e43c091b348174d800fde0637ff366b3d781f7b826b1d7eff88517a1b383c5a258f21c22fcfd2a986f8e9c3db03d47f3deffb5fa180024a4c746dfb9c6575da26ee579d87769624073dbef6fe934f86c807f5be51f12b35df7dfcdf1fa7cc731eca3b07c53fe4037d08d25f2ac09f06c5354af1af6747e69dffac4444c964bd18104f27c23b9fcabfb0a71dfa2b38f57f245974a52f4a9c17df2b9a27c95e208b7c74ad73afb0097e9d70dee50cfc4347cc2f60c25310ec635bee555ad6a067e7d56fd85f0f21917709757fcbc67df233bc5b99ffb40fe5f5cfb89c8bcb3be16fcadd78b0e6fff19df33e26fdcef9fdf78c97fbe8f75f6a78be5ba2efb3d5ca81854b623125fff0226a5fc1107c5a74ffce6cbe8a1a7bb57b546cda9742fed210ec9b93fd6d06f243cdcaa75ffe6875fde331fb9383affcd48dfe5396f0572aeac92bfa6431dce76d9d4c5dec95317c1301419efdda37119c92173db8803fd941d5624b6530e1373df36412fde387eabbfca0776bca3f646ebdcc8482ef0a67cad5fde579f9d36a7ef9b6eb7878b788fd5e2ec2ffeff6ef575d4dec8847dd02825eca9f57135be3563208dcdf5745d4c81937e91cc65bb41dd2ad9d85b5944577c36261b218c5bc2e4cb3ed8ca54736c909cbb81b34c70a34d4a7aed71fb7273a6de83bb22b283c0fd31a2e04f20cef4cd64ba65982ebf3522fbc0504b20408162f9a4014c8e56831b5ea0dcbde72a3afa7cf5e70c2bda8b6eaa7fe2159af88a10a19b63414290357438b19a87985ea53558e0091b72c3900ce7582656dfe7eddfa836c10787c52b4af420d3b12e1c17ae9caee5ed92dfa526be78ee8394cb1a63e0b187d15f44523a1b7b04da588e0e026cc45e960d25c4eba2381e41a69cb5f26baad5166d2182d4251607750d2c3ab801a1f7fef5b309afbe4cdef28e60b84e67a03a04f381d2fd5d11ff311da633108874c2687077232b202d48a2706a926b68142c8909f75082342d72d129d53a1e20d4c62336bf7d7beb2cbf3acc52dc0118e0fddac11b0f389698d8635c167bcf84c20f7067fe9d8c084b7e27720c8dfc78eff927a5661715d0d2d1e21b7c3415f34b49607274b39ec1a96e0aa337fd47dcd4ff9d89a878c9647eff2f7ab13cf324f2d395e1580021f6a1321609b34b8a9ad4dd65e60e679b8506d92d3f19db650dc9a3c9b7210019e15bf820abe57423911c67dd66f650d9c38300ace87424205a4735189752673d5d11aa5ac3e3960565cca341921605b83cd5444c72b627048f4be3cd15c70c5528a8e939887840379c3cc9ac33eb36ab25ea663fb4126db3365649a9db41eb0c5a774e3f7a9c6a569c61f528ccb53242ba4163ff7e7956be355b722f516baabdc4bef9eee5e4012bd18e08e1dc0c33d3ce1dac3f6ac3766c641733e55cc7ec6a3b550096e08439ed8440a95497ae0f0f62b75ddbced0d81ec878cf8fa784e67d9278aa6428f15319d2040c63731dba5517298a5e220de98fada59f40f14de73ff0c78ed974ee1ef22d7160db9a5637fbda4d6c1c0b8177d197c5f0f806615540c837657d684e90bb4384e09d05c6c6828182fd71d3facb05158aec05ba565ecd02fa8ee8ce20ba0f4f3157096f479c1dc0c7c1451170d3c56265ccfb7028accd63d41683991b20d94ce6a05ef39af9b05a4d41f6e10354f5a7637eb280c4935a9c09d0efb010bcb8b701b8ea25d7b3c75a2305f56b2a22c5710ca0873dab5000a4e22cf5a1072a05d0dc0d26ffa3a077a500ae66c53335beb7606adb6961a6e5799d5979a9dc18adbeed75b92c3f98343455e468c5b14649241b7c344eed46ea0562bc03bcbed1a623d63072fc819f79b24a5aef33209eda7a407c0cdd47ae052b8e5a33a7d7c8793f4107fb8dd6c8aac9caf9cb2bbcfbc0b0ec863cad6cfd0ba4056c0df5e162f2f7c971f86d75b150bd1c745f1946a843e597fa52341cc696a18f822b893c40dede8ac766050b8d1392c1aea24b557076e418bf3ee6c97e68b9824c91c1c1a71b619ed417ad01be063d78726fa61cc919b7c29262d0ee5f6ccf80dcedd4b5f8077cf23f54a31f590cfbd1373d4f14fa1948abce231d96a9a242d809122e12d616dc9fdf612ebf7c3fed460835167e5cdb1e99e0934d261f8f97236377c895d84ccd0e4a638d76168c120fa5c20c69288791363df50e5355c4929afb7873ba495b2e317847d7ff5b80a3e2e21d73be024f0f34282c78395c64d7ba43a83d1c1b811e002384b37cade6fe43a2deade900e385a277bce46cab1503aec5cbde1b1f28c9f3298cf8de6b4bfb13bdbd574360975d93a48235716ab782bd483ee016fd81cb07acbe945ea511d976b00abb6a8623a43b8b668e57ee06c52742b6c3b765f9b861dd25c30d4cc751278d8d470c8ca05d555363caeeff3246090003e84ec009db626028e2af4fa60258a1636558224b34e3d446629f62c3993afe83bd0cbe5edcd61f5846f0f445a39bb533df8eb5bc12070775b15fa5a42c3830cb1932e8aa11bc9851533ce0fdd00e707a230c2a7fd7e4c770ecd910e4e44ce86a2eea0ef6ca8b9cd4e5b98a4b5196dd60b96221c261bbab7c5b201c3b92068566105d3f80038f1b2fc61955cc4160ab9dc54e32ef257a9ce72c930edc95d776252ebc5209f1f3608c15660b53bbe2ce12d5de2ef99551f83eddf3fc8de493e37ebeb1ec0aa1d6dc349c2750fbdd92400fd2061551159f5419342679135c8c5e0902e1ab8ad2c8360e1b70932e177e274bf67a0fdc2ef2fa67302e9660e3a1a2e29741a45f3545911c6c6c7ea61435ff96bb07b28abbb31f116bf4de3cbb19ceab815353e2c141b56751f2ab09470cdf50ae76483989b1bb61fc89a211cc61d83f630703d9c7a58b4f436cc1a650783fd1002c94ca0e79102867a075e6c9672b32df294bd8e76bbc3a6c708e4023dd4b3fa1e9df097c85399e21e039e7f62d29d849e555884342b4db8c974d573b84188e4617fc1b71a2d13a389e59a14f6b5c8c2dec5522be152d54483aaa8a6ab7810b8b901e0a7684689ba52628c47320a533b41c574239dbb8143e4ca663d29e78cf9600f1dfb4bb1f872d6c1bae3e128f43c1e8e9755e800202092e62ba4dbdf073abbb195495fb1dc418bb0d671d2acb200874098db825b7e40aedd9ab3dc7383ce77d580eb62cd9ff693a10a5296454139dd81a574b660fa5cb30244ebf16deffcdcf55b74f1737f5d576b4d7f3424614b130674364712766ca6c4c256133bdcd6e296baf5e35f40bbee3684f38f7b0d91f1530f4f19ff9f2db01b397f811ec35b45561c1796d085a78d549bdb7d5e68ac410cdab910cfc96009a0e16747e43b06ae5b6671f4bbebe2f59fdf1d2beb7921cfb241e072f582e3ac44830cd36bb7dae2c27443554434af6faefa028ee5c85afc035ec89b248c13fcddcd373e3921353ed400812fa5ffbd77bb7f63b89da49e46dbe9ba58583f708340c0b6a5b97c4f09a1c3623fa3c67b2fe81a2d908f17f3d7e1a94f83ed2d16ef730e4b292367d18c302ee7e5a8833e7f117a515318dbd510e8c7c46c2021ebf19e62b7539219a7687794cca99592565baab55f46f9025327887796796ec82f232c321f2a008c463cc787663f420fe882eb4c26a1cc25fa8e4a8c1eab3b8c39576d8f1809c329bc5b2eed75b621ad161182f161d80e1a284a8a16b9f0e02d3184345f9e2887362b90f2b6def9f93d97c8291c65e961a48bc0295af8f3a966ef61cc946f86709d45bc984e7201dd5ae1c04c8310e5acfa53d7e3e67bbde9e88d6e1274d5c10ca3f35e67301f2d3a5ebcdb8ced4058a4ee041e3b206136fa16d25259674ee439b537f1cd689bea7d5782586e33a186fd2a1e6f4dce00d1b5dce0c9422d07f4abae833bd98542ee9e14e3ae8276ac76a8507418fb11e70fa97026f9c1786e36572d6cbe179bf3889bb7f32943d8f335c7a3743807c9b5b36f869a3d9b8fdb38abb7dbe66e3f0509cd81445912a146c70b9795dc09362f0252209419b2f5326cbea41e3572b9ae9641d374e6dbce8ce58581c40bf47c6b36a0d9a845f5177cada3cd75757986167b3b899aa55f1483dc8bdc071edfb239e7ca668966f15a9c07478147a5bab209e0d508ec715fd3c094d7fabbfd805ed0290447a4c0a6981276342d0d823e9823ddb0b91b2f137873183ba3a63b9a3a33d648a0199bf494dc9a2f383a9e36b26edc88a4ac1951105e696cb9ae57166aba47dbaede7ad7cb8be6bb5e5e24665460f71accc5b001db01066f9ac346d69a1fc4d1219e4eed35331c2f923631ef0ff2668f6fe7a0ed89fcb0912d0383e9769ae8b033d0d1fefc9059fac49fb03d7ab719a85a8e040ba55afbc3489600558f00dd51dc52d701516786ddcb2e7472f70428e45660005e82643680da0741ec2ef9d6d61c5bc210b3962eb4895493d6917e226d7af92416d1b59c11609bf205c70b15941de792a818ed4157450fab0dbd1c217b1eb7784fc5c04aebb2c78b66f9c60fdf81305456cb59e8511fe7cbc24df0310f4943a47177e5583d70166bb4244c7b6b778023ba3baac553ffded073855879bdd0944132dffd7b7afdf7528a002db43eec77b50e6e17a947fd5eaeab1ddb20769c66a813a97bd4e1cd74c2e608e8a6e10eb45fe78f3cb686ff6b9ac3dfb687ff4e835211c3c71b83c96cba33c617501b95402e671587aa90b9a645b675a940f86897d4d8259ec41f1bf8f4b0b04e2aec20d3c618c7663b989d61bb66bf3d1c4a16e5b9cdc3b00172032f5a611d9e4c574a87a1d8bd098d770a02d381dd427b2297470adb6b46189b981d6768d946b8a59ccda1cbda9556d0e34f969d488ff4372e883a443b77922fcab9dc1736ed474b855286cb9542e6bdcd64be556279bd9a5aed9858d37a056685e27d47b342d14b99a2915fd82fb866d32ea22f4dbbdc0327a115b85d87ab611ab394964d782456c0d1740e0ed7860d5a4840fad14eee721dd94cfa2d1a4a98281bafbc58ee4b6d5407f990a2821608c504e36407ba83f5e4bea1697dcc1ed7e476e56dd902c4935ba0f00164b7cba168c9bc0388a7fa12f417fcf0a96bdbbc732e7b857f41c4fdc7275623e8f1bf3c3ee0edaf571e074376bf4c5b7c7461916e3efe73ac5aba920327ab13f985feca4ad6e863c744bc5abe05d65ad42e522f5d7fbcae1656e88dc643aedfa5dbd42142a1e9d8c095112513fedead906355bc2b9585e3b60b7856ac9672e321f5ce9bcff22f2dbc7f0a209f4fa41010923e01e55ce8fbf2418683e66281b75d0a969ac8ebb67e3a673ccce79fff75fc1577a70dcbe52539b8393714645ef8fd57a44b6e0f7a1caaf71941811c45392007811b7c190677694a11ef7d51d73f991691ebb9a15b76c239feaa5f5ee0da7a788ecc7f3be752d0d57b73ee7dbf7fa7ef4fa241e07cf1a2875ff6f2711d6b681370afc154b335ce70ccdb83a88a09a13dd1f7abb8ab51fa4c59ae6684cf062868c6aed70d5aad6c1f76b6096e93eebe17ae18948bfb036b8bb5fd5e1b1dc2f678d39a7d33b9c8085d07fb1e9c5f45cd149241e0f4b76048ab14111bf888138c3b8b4e33d82d756e233978e4e06b76d3ed7daf650f83babc81c7a5b7560b6fdf000277b7273a3ca24a831b7dcc9026b39ce4053b53f1b995d0ad8e678361b39cffb3a2cffb6bdc9f32999f101f64db35f410f8aa3c7c89e370192125e58dafa0cacb81194a771b8f0f42392863b8436a65f7dec83dafc7e73b00a992d77b1850b38de5c0eb2e46b5389ece77236dc9b24327a995572eb9b66cc9471337cabdd3de7ebbcc49ae7dcaf6b998030f1f7e2dced0479abceff20b5f85bf2bdaac977c79110e025f97856d5f2101130212296da3ac8db97123cce6993ecd8d261d1f688959fdeed478d51baf87ec8dae3f230cbf207fa99cbb5fc82e72f74f59bfcd6a299910a06b7a080e6db74d4a8d2e9fb47694beb4d3ed9c88aa2411bc1d4bef8a5d11b456431f5e02028f4f4e093b689596f72c55459b09338b46b9ef37126c3c9c0bf138a4b7e9ef0e909aa83a4fe5e137a83acf83e335024e817a9afd411c87138c6abd7179ff12107878009c6557e9ad5dcc50498f86857eeae64ade1b937030a0c7b38637ab9dec12451e20c9a2ac27a587d57a5c8077928bede3e6be0a2b2004341cdab57a3ee6cd9de5a4158e88c1b08750583edd5528b92dde5758d4efe8f56be4c55de55e5a75ba2b48f53fe4c941800eaddd2eb1680c2977b40197fec8d076ddc6603f565fe7f37cda14654ffaf90af9f2bedced313c12ca9117b8917b5cbb4f29108f40983fc3888fe2f09466f2bfffe34781cbf8e37f955164167a38590740a9e3f7defef996c2bf445f74fef5a0b08c2bc4e1c4bdd4a38900559539e313f261db669bcb8639b3d909170b04246d04b5b35d0f28316a3597c3293aeef696d17e8640599e20ad55aa1ecccd7447835bb1a9ca18266ddafb76256f78f17303372b0b98404743b7ae5e8e72bf7472bc014ee23e2b64c9fb4e037126c96eaeb088d04f860ab998a73994e0b23cb56cc415417eb3cef61e4650b300ebf948a60d599dd8a3dade5bb4a65d6480fa4c6367e558cfa679b11f77c07ae9af0555a86ceb512407c8cf97d93a8a6b596e0a04b2a407b21895643f5dc403defd71f8cd982d7466eb9264c9291f94737e43addfeaa39b57dc77d7f539707a4985807b0863c3f578da61185135c67b733d60dda1da5e8b5bb10b755bb9d06657aa4c25ebedaeb9000fea9ad1c642c35f8729c5a8112bc394229ae3c50e96f065bcecc591659685fd3e3a476e067711b6c01f96a19b6ad02766e447a708761f991503f7b62e17fad57ed391056e561497f660cd54883bd1979efb7a5025450202ecf8b067e7f4404ca6fb0dbcb1bb413c55488308b87a39bd2f2bc8ef7c125ea05fa8fde1fbfe0843cd33cf1f11ef66460808819b8672705f3df50742330fc2cf4abd7d542d4c4374049252dd663a1de1adbd83805b706080e06c3a187f5e955f72293df1287ddf3cb8ca3d9e44be6e8093b8cf73db91f3e56a4d47bc66284d9b44a4fd9220e2468b6bcec856dba0258734256c0ab1ea6e2a4c799d47d4ed965b8f60c56fadf315ac8ec714b1e027c42c369724e8d9db5d42d573ce576173faef07a5862110c89ec58bf2c7c4e33a5939af5e50e8f9f97195fc1c0820632f516199979a8ea185a006ba3d1f93427d2a5748af2e5efb78f47f8c74d76ce4e5d07fbd29a2dd1f1ba4825d95a0e4512b9f6bf248e806fb1edc5a6eb27c532bdaedb961748ebffc177e970c53c13172fde7b283d551248cd43a579d8583c0e50a3809abe63b0e5dce84349c45d6e3e53e5acb6b2f813070e049162c92a32dd2eda4e8b22398de92d52753b3bd9b82878d484113aa91f5a2248fe3647810324b5f724c9e980b79c1e2fc9b1accaa8766ed02e30fff425f9c75dd3810e50be770015efcfc9dd83b1a3fe12957ede729db14ad84a0f7f89fe5d89c8f7eccca87e1bb571c0fc3770f00ecc9962fedc199b1365269cb4468b33566f48edfdda2ee6a18aa2bb8e777fa87109e7aa68df2b39d60d88d7d6fd31337e842a127cb008dc793bdcd37e51c55304b1d0526d9e4b7cdd9d84dff5ff6feac4955edd91740bfca3fd6abd78580748f2a602f8820e0c33941dff7bdb1cffeec37c4b24a2dad62b1e63e1137ee899831e718e84ccc1c5d8e6c7ef9af47f02e7be4673cc9d71ba861181832fe0937ec9f870c7c10bd6c26d7d6152cac4374c0dc2ad801c9c98378056751b374e62c5feace51724f035295987a1e4b216c6ccb9d38aec74eea265230ae16eb4c9b0d78b33eac464d051c401a1ae789bedd65db13ef6cbae4df3aa691be33473c42cb7716c285e24504977fdbf8b25ff28fc1616d61b962a832a02a678a8acc068fb4f901a6f3fd6bb5ed9183e77cfc67733cd8e360784ac4bf92e910e4616504027aca3640e6128b6d99b2a6b8e5ce80ad4c39ba82480703e1c41e12a29653673ff1b2c6b6465a161e466be9b0e3486417ec742596a8d8ade763623059db40f1e6a6f72884e0b2b8875af50e09afe701f949f6228bcf4eb7033209ce67355c86f2c1d63278919db810649d3d98adbd5ee160d7d79b69740be97a1bb8f5f95523b3ff685cc03de12f89b4dd6ed101d07477ac8ef2613456e70b04d699c346d85296130b72bfa88e167bee13a8e9d92d76e3eb4b6af03ff5483fbee21f80367714e91d7d6078df7b21d03742e5a20139a5c650902ea7245a9bd30c2b6835cb32f3bdb5b6ab9a70fd3d977fb25b55b18e30b94fa2ff091db39fe4ee5fd0fa26beba2f321edfc82e736a942042d19996cbbc38f15b104b9b2a92ed1f7cbabf1dad0f0bf58f6f461fecdef5ba6d475b1082f1c428812393c48b85504c963ae62a308ef5b20dfcfb95f5ffef13c48fb41617f78f6fcf2de1af29d276bb6252a868361a073033c8e0d9fca026e4b261c71caa4caa8921fbc78418888268c4a8b350708ae74ca2311c5018e333641b98abe0484c9c8d2c4fd7ec3601c4012072f13e98f5b3f8c59e35bc15cef966a5bd0de6f5c4ff11afc0098a3428525b797b18f64b24b8a3db0afbb3d725a9001c7ab68364a91745ae53b31a8f291028a9cd561006afd11e1f790a75a336b2c7c4d07f9f73f345f6c2d167a75bd60d5e11b5ba1ca7a391bac8c3118123f4f1949893a3d5c189d6beec2d37a3fedc7cb112992f1cf5af0c1b7975560fdbd49b2fa5702fa6db9809643a24f660174bcdf722dcef0b707767e44ab4e5e4da6c87e4d78c84830aba5bdddc6ff6d5aa5c516b6a21a2962882aad94b057d0f50f9a2dad9d322fc5eb3fb7dbdeeee72b9126de5726db6fbdcafbe51f95c65d68055f68930230e5c30710e1362369bc97a874402276c65f3eed6883e18c2bbb37225dab2726d0e5b4abf6fd92728e528479aa0da82e388f59451520e168b8d4f9ffda539a196ce748cf83e723828fad6c2c484b1c1ed9a72dd50378b05625492eed4e2ac4ec2a33967f667d670ce793f436e0bac61649a121bb760b6c770cd27bbd383c340f39da156a4d96764ea939ff8f2f91722fc3755dfa873236d0b9ae84e1e7d1d1b0fee68d3b18ad4c85e53787612810fe15b4191df876e5db612ece1442ac2a19235a1f6d70dc1eff1c0aadd9b7173fcecff7e02bbffc6f953f110e45b58ae9d468575fbcff0dfe81f075ee98f31f00e78a523d2c065823b5384d4153c0784246697c0d2cd0c572a10979993e9391c83d5aaf1dd8a5da0c871b6543d74004ecff472bb9ad315bf4b658907672939988e7d901030a572f678ea613f18e05e5e673ad52b791e997f16bc13df807cbe3a2f30dbde8a4928306bdf9c96d80ab34224d984e02e86e744a9c973fe984c073ecd377948ae4a1cd6759c872065be1823200bbb85c143c0de0896e18a34ebc514e1750361ced61c64febd9df2091868fc8f2d95e1c7ba76de21cac23db5834fbaeda6fbd96bef061db25b76926451871914ee8eb930889360461828874fc179bcaecfd9646f9b666923930d4624eb30d2f5c106cb436a1ff08e30398656b2517c563d24dc0493e64b6a8384337dd44f55d60d5329fc7c6829b9515d43d8da2ad18f802db1a2ebe9572233f2f79b101b272c95d451de62473fd63fef2eef0faaadb43fdac396d6efb28eed95b93f4e0b65b54552858957eec1f41c129dc6ea8c1aef3200d828928f56733917a6320e87e201e54c229702c8c08fca1a1dad2857f67595754c60024dc11964edfa9d717e1465c6d008cb1bfae00f811f4e581a693ef4ca3f7bcdbb51bd4af2daee7ac1abd8630934c07e0f088e93ba4069c3113b00c56a30110d3b70400b316971ced2e8329e4d67e431b5ad05b72dd38da9cb5a45daf07ea5ce25521caf26c5b9a6fccd215977b2b4bf5bbe603f337bbb6ae3d6c3f0ab81bdd43169c2ede3d9cc9d4464186eca95292618b513badcb5e21fb32b7b1a73e2cf43f1d6ec66c619840e201d9098c23dee1c0fa66121080aa6ca1cf35aa57f62e56b0b783d147d628f3ea9b6cc7cb4872daddfb841932da1ea207a8e08401d90abc6e22648753a3b751745fc0b107f78cda3fb83e3f348fb0ad47effa4db6809f4763718f378a3c44419da07948ce0822fcc75f2dadefd8d3f4d33b22c4a87ba9169a913bf87b71cf5ca757ffd8a0f6ebf7fd05a367ebd752606956528bab3212dc40a7cbc071ccb91722dd8f532f2dfc3673d54dcfa33f059f053e0d2bf81cf82bfddd8de88686b44767c3ea0855a5478c305d08a29620e59b3f37f6db2ff97f05997814f53a5717eb096f54996bea3fb31bd3e7ad704acdfe6948e07f29e5ea109d3d4c9268e8df1d413c3e4a0c45d8c32d9507542256d9edd387fc062f640facad9dd836e9633dda4837d7a98e0b211529a25d036c0c76bdd1757bd10c23ededf169fbe70f76bf997fb29f3ee84eeb5b75ca97e48a56dbf48297aad5c6bfa6453c592c707c8a92c550f8b2a463894c772ba26ad358b0046654ad4390cf6b4998d4f5ca0848cb19ef22e77743d05ded3d10e95560382868543622e125e11902e41d44ed67abb3e7260df49a387fafb45f72a8f5baf9548071538456222ab57ae1c4ed28dcba8b13b19d70542ad97f0ac582fbc95b4b579ccabd458689634098608bb9d64115b8a61999f9cdd19dbb89e4c72275bc7687f3932b68e4b7552dcb2a1f60eb3b61f70424bf1430a4e17a08436900acdcd4cd92b7b075eed3467ba4033fb98506469cf8b7c7410a41406150b67757c3a558f475f9b9ba21db301c5445b945b213c6dab4d5c9a48d3701570944470e988fda2c835e706c673bd47bd5f56d760a32c8f7e527e7b69275f943f04f9d9ef168f780688c5bc4076a016da221db0a5bd160df0ac8c46422f9164b79f102a8171cb4d7f2a70f41d46ed863f73ffa53bd4991721c70f302eaf02f32df5e963f0e9e3effffddde8e94aae74d3bc7aed074fe4afe3f8f4b09b2f690662c1817498cd529e5985c1e3f2b1c448bc9a83ff4fe3faff058d4b577263f809a7fb47b5933bd29f13ecf6a09b7612244b264fb0b40474a0e4d9905f4fd42d68e98cdded0ef33f7775f9b676fed145e572a8282529a7a3c314262b863867dba81c59eb1975a0e5641652633b01c66798d705bff0a806c989d948e6126cb149b31d02ed265a22ad15233666e4281c4c474a1a1ee7f0a2dfa1f2f6b677ad7cf180f1f1726fba7eef61c3bd5fc8c89f471e467ad73ffbbe90916ef5cf5add704e5169b84069a8140a89b4078caf6a6b11032a4633756ea19cb0a39827b3736e2e5207180ce475c02220a38bc69c2d9219a32aa8e7410c6f20fa083a06fc141fb19dfc016fd76f9b2a16a5ef910ae19eabf746f86382dfba6dbdb35f57aecc33aada7020b0d04a480b65166c685daf8e87a24b5c6976bd41e83fa8c2a35ef5851f285f19fbeab72bf777ce526a3f8e024f13e60c9bed125902171b77a203cb2e56b0fb3277ef4c467dd9fa2aebf7d1795162e095cd6459cd04d0e0f019c69b0bc52e8c44d270eb3cd876287fe26443b3f0fdd68338d422dd18c691f383f9be97d1f9f53baeacbefaa45b4068846dd712859f3c792b1a498230030c1d133076123a00a07dafd0f8de67d9bf32e3774fe52b44f9c362a150d27665cf22c162757c398bc5ed0648f049cfc3e0b912e3bbc08a3becf977ceb15e52b852bdcae1daee564762c582ce44e1c61b56a065cddaafa01437b98d449d7a5d2cfe9f2efa7f59178d54f74feba091ea5ee751a4badd744ed6d3e2744a98b0b5d240c2421d114ac311114255b76da1b5bdbdcf06e8974f7a47f7caceadd72d8b74e448d9b2de168e01f32e1143fa62136c02a9a6e6dd4c98d722c0e12f6cf51aa03bd2779c5d1f7419b03657dc3d821c2f8e710a9b895bcca08015b45b95138a3a1a5e6e68e4f438a590850d875b70a58fbcd25ee4128ab3295c67a48923589dc3f278e2554761b3068f5e81428ad2cf1dfec540e6e8bf164bfef6edff09d15e483f88f6f2a0db5a704b1186fd2969b88016a77c743208343e36238bec75b9bfd5927e12cefbe325f61527fcf9760af5aaa9f144fb433c774f5a15a28369d446d8d8923cd1c935189f4eb7b252f1f2565c2de8d3ea184e9382a610aca663778ac72bdaa151eb34508581d82c8126911c9a43027d9359f24e610dbc5022542809a6ef2deea92ace0f726d9112df57d1ef6918fc20fb21cd6ba79bb3d250d631c698955f58ec2e5f3738568995496c70a08b973cfb155cb9d70cf9f4926777c0cabf1ac48e9cbe0c40aef2d4194f5abe78dc6207a9f007feaa6f05800e0be5cad42f9e835e63fa45f926895bbf6b7188c42265893e70885852667140f4ad8b46fc9652d3f4a06c47b394f620d949004b0aa72549c45b0572e752ddd8f38d59daa7b8d89e0c109fad62ba9ee4f08ccbc6d06edb29332ffb8e51f64764f2094af6d9ee66f8a6f2830b0b4834de181a3a86707baaa1e804e21aaf43a199cbbbca3f5bb0ed4af2838fb253b1b6cb805aced859ac1178c341856b6f8e47770063b26713f34d222f2474c51c2575332934650ecc928aa82b9d665c720b2586636e3230a3c1309624ba248b518a2c76d66c9b9efb42b3bc0022ff1159deb9d571fb61b7e811437da3fa21ccb6dda5fa505be1e180ab2722aad151335b6d1644164ee3655512b12c4c3cadd8d88709181969040a53a42114671f7313bc818c9a182313269f9d36a0bf228da507cf914d6e67d5728c6efa15957c2ca6f79bee9237b1a13fa31dfe1165f78bf255a05ffd6e0a2fb2089733da35c969c6c17a1cef59dfa2ccb37288bb9d279513ea51f553d9b85e73e483ec95a78f4ed75962486b50588609a4b281db48a8011f17cbf37613a9da78458c9df1440c078b38811901597b2937235066a27130cbad80785ea001ce73e174c112ba27ed8f190fcc98e3b2a313b6cade2f979e7b68957dac952af3bbed9e8b5af512675df1dc16ca83f9320ed2736d91641c74bac4fc4fccd36734ce6e9521919d7ea0adacb095ed34daad8ee871cd5ada81af3b99468d4071de8d463f68f20f9a2d1f6dab0b04f9654a9ed789b71e30037d9e18a546c0eb5da69e003ee1055a1fa3db4a746526daa6ee727b424043c0c87c3f87c540270efb3d8f3918770c0b9ea51c99121702bc3da8f50ce4fb29bf7111365aa47fe0ddd7ef30eb9dcca8dfabbdfd2681515f5523a336ba191e51b63cebbbfaa8028e60d8a496d15a2e9ff16c60bf36d83d71f0e375a867407a76770ffab8007508450787bcb735bc9452fc33ed2ac90269768b6dc942a9b1eb642cf9513d1bf5aa1d7223dab2f2a19c8d7eaf13020eb9810c18e551190483c3f83861943d3a2ee8b5363b7538325ca554aefeb0e135a7e6274031b4c7347bf50260f8f2718bd1f9eb24a467236e718e0b5641d9439c123ac1c947558ccf4c076031377a1b9533eee56f7223e7c24de40caf043a14f8d893fa76107b4c1a4fc2f1261e84c66e491f61a52ec163a8c02e8cab4ea6ee6d8a67b7939dcca9f48a161ae6a431335f2eb7801dc75b7b2e16eb034dad89d24da703b5a7127aad92f617daee3bff9ffb1bf96db3fe0b7efe2c8fe2e8eb83979b959b0d7da334c2ccce8db7d528fa05c83d926e01f8ef1f740d9593a0e91404e5539e72893ca6c2bdb69110ce8631f8acaec78a3f03691123b1edd4de4056aecd404ce3c31425037371320ea30c4af9852bf0dbedc29c6316831f0e7a35e872b573b36160649962fd6434fbe791fd5f64af22f9e8b4e7faaf91fecb8a3c1ceab472a3509f1dcb69bc0d5767bc586db8d7de87678692c2785bf0b9dfd974237a65a66d762cd0ee2cf9f35866b9089faecc3af39372aacf847a20f48a0a3594ac1966b9a279bf5d2adc6c98479e11bed3bcfb39d63ea95e05716d7771ae5de638a7af04d3d7285813ce6b679becad6c568c509a2d08415fae98916ac82a6b2f76630f55626b953ba616cd729e50b59a89e5691e939037dba207f56c8d674a918900bbee523dcacd868d12bc57f5c03ef18f1f44af82b8b4865742bfcba108c6a83d25aa1db3f62b6f613945431d278d2a4ddd8a168ea313c0298b95ea733a414a7e5e1804cdcbe579961254ba4fd7c4443f235b616c2dc4a3b2134c3d486778d233d737b55a68e4cfd9f4900d6bb4c8a0ca5fdf6b5e3f89577db7a7f68b2db8106ce5aa86dd220962e3245485e8c0a9d44c743fc3b02622275843705d8ee0ccc8b4b7bb05d243736a29b60c1899366c69fc3e2b18b9c024eaec7ab33d3c43d80cde2f4fd9dedcc549b46a4ea70941543aa188fa08d9cdf219128b8c77b0d750e06dd085b4e75060d640bebad359ff348d8bfd42a250d8ea722b75b328bc02640f5523cf8df4371cf43e57f6772ff9a858f3eaa3aed7f9204d9ad3da9410cade6076996847549c838dec86ae51c8e499b46d6d3211d20467231cd94df7d3153b3891028f21c9694fe04b4f88a689ee51b557788c80f11954a55da2ccdb1f9e69b6d12e91775916ff1c11e18eee4d3cd75e9b65f12b2e42be24975e212a9b4a3cf28c16963c9ef091558676a7c5f0f5b63c552ebcfc0479db676d7f7fc11397b7c72d0a6e87fdb456d5d902d167b280f0f846a36cbbb0b1c50ac7175221c249b684e5d19939d16bf3442db458741602b0aacf1e0a11d334535420d83a8b7dee10d5961de06384630df8d84d77bafce8fc564ce3e3d25039b91d15f9f0f2e9cf68c87da5f7cb1b3fc5f9cbf7ba792e43d500fd9592b3e7f300af62909e8d72af4aa595dfc1c8f2f13b6e3f2053cc77b309f937f2b8a7ffc5fdfdd33607ff575ea95871401910a43ab0886c3c057d6d3ace34302f3ad4bdbbbc15fe01c2b1dfa6097ff003b7f88dbfdebdd9c97ea49330c23a84b64fb0303b959b0379048937e0e1df3840fea82bf3b12a590707e6653de35b9bb0d1fae838ae0ba31646d6ce401e1f507b373f1268a9ad4e297048fd850d6e3820a64200778141b29caad9782caf7d94d8a4a696c7e33d3998e8e286df26d469d2ef42dad6fbbed5ebba62c2bf5181a2d074defa0cfb2adb57a21ff2bb34bbc5b1d9f03c192f02f534d1533dd8b38d25b2e19474d9ac5749432b5534c32cfca1997da882e01d3ac93739bcdff346bdc5f0b5ab5d16f4a88b10368b0384798b13bb1654034c0a941548b91ef9ebb0d331f88189ff7aebee77f05d48b65c5c1a43f0f7c30d1c6e792428a78856cd11d2c1e7f0e1541512ad6479d20b9ffba916c0b7bc1ba3ce2f3f2ecc3f8abcc0cf9502ee159e27ede62aa38f9294e0dfe0dbeb82e7fc9299d7e720b8116d23f93e73f23ac53c6dce49be59fb42e5a494d0f840e8684e7a0c760e5bca19e623215e312c8b99cdea180bac57ce4211280b479ac78d3f17fc9025c3290683603ae1ce931cdeda5915bb5dd487ef818ddfc0ce7bc8e139a011eda6456788b761b4703192129219d065a16c249c24dd1388cfe6b4871d1b6b311eeda3352bec6d716c4069b91da595908c727cb927994a3e189bf508148811e39ea77c6040fb7797d20729f84a116af617e0d33bb0927f3e271e2803c3c77e0b5ad2617630e1e088ada1e2b446ab3556a304d8680741dd9000ddacd7863f493d55996c0f879580af8224b0ab1d3408c215b95bee8a9578486d6833f2486f70dae7ea0cb01851a87aba94ef61b79e5666661bbe3f4c8a28bf5de5d177312e8f52f8b594cd1f10fd63319b771f751d906c2430106051736766190e0c40c20e3e6c0bc10563dbddef705f8bc3d2da1cce106a4ece3b91dda33b13f0686449cffcd01420c38ed14d1aacf5ad00a846693003e8dd72fd65409ea7ee1531ec9ddc35e73dae759f73f142f0224fcdd1bbe2c628254ba9388b53f3b092d633bfca7682953647d7f7352ec9b7b23226767ca6ee154b2e7890d4a4d0070910896ad18d9a792a71e87901c8a39346a0b598b15a44ae8d7e96a77b549d1f913f7da37c6f5dea53a7f442f02237a30cdb0c9c5f3509a60a0e7c3e30676445792c2c549b9a5a1ad3c589e95599344e0dbfd08da17f2b74f8548a2d6f6263a8d9c6d5c6fbdfa3f6eaf05a34d712b86636d414cdfeb307ea13edaf82bbb727dd6e8fc76206cc369034dbac12ddcf2361654f9a1db602d4aa8feccccba1767dfd0d55f6d1c478d1d93ddd49e3bf3eab29df4477f7b677a906dfc8bfcb38787e6977b93ebe02183e3d7861ef7c23d96687a188d4ac85c4096d367417b4b9d817011f6dbc7f5f8c3308a2f026e0e7f4eb2721ffafef35ab9f707fc16e58e3ad208af807a93f2f957f28f522be89bbb8e6d777caf3501a545ea673e76cc55602f122c59cd720df34fcfe7db9d78e726ea3b88ddac9f2d76096d72f39a1e7187781c69dc1db1fc9bf13ea33506577a1ded10786f7bd1717fd37c22507797662495c90214517e02386c999365b418b51f6af85db5f6e9f73f7fd44ec0b11f941fb22af6beb45a4d31b59195ebe772250cd723906298d56013e6e787a8b13ff3ae1e86be9fdbc5a7f3a85d2220cdf26b0410f78a9fff00cba52fe3a81aefd614bf3779d679505294e709a345f564e49cb10486b5cea64f2c92c375b5eac118fd96883155fe7f19499a3f576332acfaa861ea8fd1ca2190218d31a5c89e3cd519b226430377d85ebe21e785311ff1979e59ffb077e2a840ffeee21b808c5f4e2336eb87a486de7700663d57c33c29cc394b408822148a4317d8ae0e6bb056c737a4d1c984a36c3b9ac9f18617972f21d4e0d28a8f412782b6ce56dc9ca3e723af55304efaae623cfe5077fab987f572d1ffca7e5c69e8d9dafd678bf23fed1e8f95d4d7a3b2acafa3016f72e6b845cedecb319016f77a65b0f42951eb3135d3e073a00ae962ebe90cf89a91638aa1a7cb1b324f2cc7aa1e88dc25131e1fcc6a84ed1381b0ca8038cfe01f0d7f746d06edb83a6e4c64fa05dfd42eeefe8b68be0b3d735f81e9b30d818afc47069ef9249a1e40d741217de7ec11d76267b5c66ecd2ac28466420681228d1311595d267b42d8784677eb393cb184625081a0181a270596a68e01c45faad817878fdfd7fbd2c93f0ac26c03f5c966ee8d7afadd08f78d8dd25ad5fcb235c1bc30f3a1d8a28efd736ee5811b217ec13e65b304ecae2a4de96ee61b29e8f2678353f1c9d801d6187a540ad21811928e9815babdc49cc0d7f9bef90e368e1553692364c1e0187cc96975d2c6bd71ffaf707a26ea61bc63be8a671af18c217f43fc5f3f07438ee12415866d48c1f8b75795237a837559738bd06bd3daa8e3b386d3edeea19aaa26aca5b6fedb85724c633f12f2e3f1f0dc75d2234ec655037d52806c6d649da626b6e09406056c5d29bfcc7572c064a5c2a7e61bc0f69fa1763f949fc8bc5cf47dd46718e0efc090dc7b048b0ab40d96eb711bcacce2b65f5faaafb924523889cf34f63f8cf4dc28fa4efd8bb3e68c7ef57bf62b955a9c05b3bf6043bf9c0d645f3d838735631033a60bc7ebc304f953033a3f49d5fe55f8ddf27f12f063f1f751b3fca9d7acb523eafd6987c204700436e5309d2680eed104cfef1ca227492b7dcf529677f47f78bb14b6f38febd903d381ccd56bc86a29090c0d372b9f17c7a8b24d40ae4e00e4e303fb27e29cb0bf5ca92b8a3dbf2f4d96b4dc01d4eeea3b19cd099df2c8d945a6da4c5d931f40852956cad6ed7395fc1a51922cbf37c24f1ae3d3952ab66b70837508c95daa09a92077d5f08f1ae82b75081d62ab1cf263242f54411bcb3c9bf4d2bf523cb374ae37de60fda6b6bbe12bd8ab06db6f1eabf6ec563b63952e7bcc04375d72c6d1c2a57ebb8a4c501db213cff0938fd9d7bb4cfa4f822dcb2f4d56d5da51da685df14353b4f01d1b453df08a0d90610b450d0c0b396ac27cb604a9e0b4fc66484069abdb3d3bd081d98dcc628fd0a5c58dbe65061260a82c2521e8f702aafb943b486fbc50ddc47df7e286cfff9affffacf4f51947e54b545fade9ee0fd204bbfc8b652bd75ba009682c3c069d2911a46e3357a62bc1206655d63b47de1265d8eb4b4f8d12e8df4d3486e542fecdcda2df0580785741def26dc7e022fab72e0f29351910bc7a39ada83959fa6205d8e0c049dcfcf139a917698ee0174e928a4886f16c11a81485f5b8c45160a516b89973b5031b2296d45443f675ea3f89fd7da47489c0721068af76c8b7e9c137d3c2037a2c0f0b339047ff7845c24085530c8618755ee18ab55b8cfe71ac61d37d021df42e55c3e9a89c09402592f5075862a2bbe666b9e1ecdc3a5356b0a2c7723cd73047122ef39b556b0721dc2e30cc97a9696bbd56ef8e13e1428f150b18ca1e61bca7bebd4e582fecfcd304fb45b613e3c696df91d8c31eaea5ca3e136625196dcb0533ab0b041cdad0dfa54d26ab44de77e1dec5c62e6971202afd3a0dc22aa444b94e8aae7b1760627743870c5157106c8d5e18cd96936aae07eb3321eeac617eed08f62fd69698f7a95cffaa4fa21caebd21efd5e400b1c564d42cc88d3413a6534ed8918df2899a4056b52ed903d777957e964ce4fb09d7d16d907d50f6eda76b7bc566cd744384f534840d6b90a4ef1033e32f4603718bfe6e69741fd4843bcb1f8f3c8eac84f9525e01ea94057921729b48d76a7f92505a88dea75727620d63b56e379c60af99596dadb515deaac113b317b30e1254e2de3f9daa0376e0eb3fe6ab324e770815312029f9685eaa903448006704a6de498b217f659f7fa29796d3d4855c9ded867eecae2ddd4c0bb4fdbe8d33b382fa4bdc4bd117ed84282fe5105e046b41d806bb32b5a397d90d518f69bb5dbb83085995b1f4de552d0a5113d9514bf98273e1dc9d441dcfb8dc0273837a8e251ead17524d3c9c95fccca4c90e66eb30a36fb4328f1916c33872e314181a13b4a0b55f07ecbee75f07dd1bd88e3abd72d10c0ae4929ad926055bac0c03e5b13d1168460eb1f47af93b99e787a7fefed135a1b1841cb43d05ee03b1cdd4ba440f7608d553533f2742696a75308af0b633121e538438abc01ce1601a333678eef7803c9561b9c72d359b1098d821461d6dfeaa7e99c5c86f8d449a9a31e95a7b2df82fa7e2cbff2e33b81a30dcdf0af9710e1f1f0017de9c71092c008a2b4b986affeb94c864faad761b8b6bbe42c8043268eac8292fde5805b9cb17ae06016030606501f7b85751a691a469f427c08c94c0d45bf65017c143dfce9a61a18a965dc2112ff9404d447624fd45bc93d3deb1657924d92e428c2724e1d237a8fad46aacfe380b72da00e26d3eb3bb3a848356318287f16d6ff99f8278f5f8fbae23ded3d2dc658a8b1bc3cdeb23614ccf627c5845d7b8792f009488ea7c54117cbfd00a24c6461711376ca20d804c306d42e18cdd81a1a803303d8ca6b076e8244378bc451fb29860fc2baccb3b7f1852db3d01f75785e49de04097575725aba208074e522463cf662bfd202334f0b3132293a477d33f6bc14b671743493d4c994d0607bae2208c56c20c57746bc8e44865a8a230a1cc0877420a49864ae63aedbf195db91fe7eedf4b1e27ed06c85d0b65e44b0bc8abf3a041e189e046a8469f5e1141ca39d4a8e05057813bbf6c887a3a551a0e4da7bcf1df837d863897cd2bdb0f3d9195ec9fd3eb05bd1f12c9f08892d5c0dd43893e185816f061561d0040ce335e7e3cb0983940ee99cabc3f9ecd054239c23a84e533f9aa2e309a9f2265e4cf0924b923c277926d0b47e279992a643dd31afd0b6cf0556ffba62d914a19314c6c7ba811f3ddc6aaa689fa56ce1c70350374c276c23586223cd9bcfbdfbd145de826e0f335bf1fda8ba33273c7ee70e85f8a99e87992a560bfa75bbd05d7e25f4f84beef17bd167eaa1127e4e92ab7f98b8fff87afbf83b763e73d6e1e72af096510fc3287f7dd067a112eba9625d67dcff1afd8d3ffeb63cba832bfbc992e2f8be910e53457d8b0031ee77aadd116ee7f357b74dd7e960970241c4dcd8e5e99888d58255677e13e63575348fa4e913e466c317ab32812d880198dac127ce2e2a268b923c9c178751e9082b6d7044ca0036120294aa2424f478ecbedba97e99d06a788bc6f8369bd5344a95507f116ef124e8e07d30791fd3f085602bd8c0e81af4e378534dac9a44da1f16eb5c73669a9617846cb22a2d9fc6034b5d500135dae3ee59e3e5bd85e88b590a5b50554d4e2e1badccfd513a25e85a9ab8b1c4f02602682e8f7509fa697fa4fe0ecd1cfc1bc67a0a60a8ab3719e8eaf04aa803e4023c1beb2aaa5a7bdb5fdaac30c64d21d78fc13a34fd646073baa263cb666de7b43b428feceea87b10673bf20616dcfdd2cf227c3390eb494e7b91ca8f8eebc10168d42e1ef8f6875eae56ef5d47e0df509fc576a37b9346db195ec9fd2e10d8d9a0a45ac5cde6ec7270bd38d03bda5089dd19d8e222e949309c83737a1fd28cef8b1e1e33a90d6c0ee91461243ff4458425360b0c96503f120123b2b4018df8ab9e97a0cfa9f2d77f7f4c8c77ebe9760f7aa752f59a522dd1ab14db66ab56759853ae898f8bfa5c3985b33cae1d66139bb287edc6287f68f042e5a330080d9928d378b19b84f1683fcda1692d1e099d810177308d8e1375a0207354265911de220b46df8a5d603c022774865ad6a2eaa78ad6e6b1596f37f3512f7feb9b77b4627af9c970f4bb1fb6bd7d17cac64d8beda8dcae6d41e011d0dcebfbf354e6a531a16f5cbc42f1c9869a18d064ecc8bb78391b1b5b4543b1208777e7462b5dc6374718e0c9f58ad4461cef8362d24f977f0a4c7c51beeb2311fffe1b4f674165a8b1a2791f979a7b32b7397cf7bbde85e03dbde6cf47dbdebf00183e74bbc6ddb6a709846d1d6dbc8eca2de087f3acc12b9b9b2d6c0d3b3ada4a71272e9fca1608ae89b379924279998f3d841e7827af72c3d45e9f378e062c36c2d805c1c0cdced01cc010ebdf176577cb0f8dec3982fbfae1178ec7f701fcfc8e67345594ea374725f8cf82fbdad040c577b2dcd186d7ecd23f6b31f84eff63293e3ded9aec29e42b6d428e1b4a99e72e6b9cc58221aad938ac84f0b800706019c00db5d8b36315c534c927817d4ad8fa929f09604daa074ab22069bd5358dccd38e388957b7d34023add09ef7fb39636711efd121afcafe575ff9667a9dd7fd6cde2426493ed62922d979eead7bb401f8c6b7c4666a938eb1036707df74f17c93ec8e29f543fb9bbde223be1cd37abc5714fd94dc26e04deca89290eb352818a705a8fb765b9280f42c947911557d19c6484fda021536eb4e0a27cd030337dc660c5d896e0583473d413a7532db147424f9dfb72091c1a75ac843781b49b29f64e21f88cc7fd930ac195e84d944e96775108c061044e9699454c1912cb169827105b50279028d8471da0b80327cb2e7fe2f81d0c62bfe0e03bba2d439fbdaec1c1f518030fd9d115f229976ba53e9e9ed714e1d1b30100f18668ad675635524f5a05aa840f49f05e42ca9968d183b57252e400c70e1900a48b504c9b2226536b530607afdfecd0a25053f27b032ef8a0da5c6815b16fd45ffef0f1d3fdff9a09fe40e1e9c437fd22b38755eae4c6e3f79ece7d338d02e8af5b39a3a730e554f17dc31fdec7f9bd78555c04f16b47dae5931b13e0dff0a325fb0381d4503e0d0d4f74733b8d0acb867ef50638b5130e7f8809067b016f7e916d67dcadd3ba3b3b1c4cf869996f160665290955ea882f4f25331853a7033d932062a512eb813f59d8e4facc19ebe97eedc2dc00db31349abb89ec10f9385ae247c154c425bfda07385a85d27cd72fb0c28cd2a1f3e57679984acf05e57a245ebcab49f77d18fa2660fc5c9bee9f2464a4a1b4ad79d0b6a3754478e0e6a407feae59e92469365039322597f683c81b0f1cbcf00154f4d5a36510aabdc5c64cbc63f7ec762ef011999f076b4901d9d058d5cce45f2b7edfebb9dcec7aff4475bb6570bebb3cf558022dc9cbf46f1bedd5e8778893c971c430e9fc0c0fb61c649e6dcf151509049bfdeb10d55f6fd55fc92a979d1eff2753f3c5c9fa5d32179a7d66e5ab13b6a5d625f9cf968075896f11f320fbc56c3d930b3b89d10997d1a3be252f83a8348697c3a5485323ccfd3f8b54f68dfa85ede767dd34cfd2a926121135e3cd5ac1cda96e52d97929d4d502e9078213a791aa7c9e210fc6642d8a9b6799bc3a05b387a372a8e451e0689fdfc51fdcd8ef33a5ff4a9d2055cc8ff58b3cdae5d322fc04b2bd9285df9c673f99d87a0c5c7b57c8ba54bcbc6c91b9150f741fd1e61e8417b9ece5d32ca59309b6585ba96db199859e00cab4c5e5d8f2c14595e053601ec8c028924e6848e3367934b20340c7a1ac9080c788b6bc2f9b4e16c6c2cf1d4dc9f2a1fe16e416ede76ebca77c11c67d7f8876733eba0eba428c50dd36736eb19c7813a68045fd8887969594ce7a46ad7349631233b64fa77c9f068b75a3994db8828e98716485c97867afd023269a633e98ab7cb5a68829dacfd078f9d5b1a27946fea9593d7a50ec22f49abbf3fef5347b904266a4a5a3fd6ce2ede93f7fff9e6f63f1f069b7da17ce09e720d12e244bda78ea4e972ac4cd61966df65d02078a7bf5f81dc419d683e74fba2d8f5f3b4b4befd7237421311e56d9059b8c58bba4a0109dd2c6c830d00eb085a1f2ce820af503460e9510185efe1e421d0191573276447dce3be46b6dcd970b06d91d0f7bae1c70c701bddc207149461b67921f3644bcf667484db0ebbd7622b3040bc63433d6112cf0ec197a1aeced890daf740f59d1bfd946a33877a250f1dfc8e447bff7e5620cfef38de593ec553e1f5eef2bb10e39db716440fc885317d5949f0a8c994fcc29ab909433420b531114671450a81c829ea269f021e4a9d454017b2011e3a3745ea2e3744d2fe465201d4abd9acf6741c524b3beb0d1ffceebfdc73cdb5d9cd70f45885e5c697e746eff5ff75ebf71aa864a5ea44a5ba12b7e0fd6d52ff5e689763b391f9e7449c00187871405b05c17164d15a66c16976ac9494b508d59adc32e645851ee28ef21e046bd92a4bec85eb8faec0c475d12a5d6e7556d33ce69b9a724a3b1451693426a96eec87587fae4a1110d95ac09df238bf74976fca4dab2f3d11e42bfa737b66133b43dd20151c14a6fcdc032bcf1152d9950c6623d0a34ef989c49c710d63c3f58cf6b67705ec6c631a3eb26f0960e17ab317976161a448ca4949880cc9809b0288f3ac54385ed19fdb6b07ebf0a1337a217497c34bb549768c32d427b8ce886a12291bf933143c2a1a9c732d8e15073c8e120af1c964f799d168e7235f1f1d97a3b417604ebf2b2b77038d7c98e5aadedc185a46fec1a4aea7a75debcb3b53c0a22fa29c10bea0512fc41f322866b6b08fd0e147c91421af027c23b0f4c389d956b882741c100a2931f6618338f96223b3635d8f7eb9a937cc65bac239165c51563d272bce415f510556378bf52b965b4d84b790e8e8ebedef4b3383de4beddae65aff7c14837866694be2d577251977a4ca64fb2ad1c6f9d614badc3ca0ad4fd0adaf2d481777378ee00703307ab79bcdcda82a92d9b5898cd73de39d31cb174348daeea436cc6a3325f9e7c8fd67c2aa13823a8cdf0a09fa82d471c9384a2bb4d28dd183af1bb4d86e87503bc11bd89c289b521d1ed36484f9ae5ac1c654b6e551f63782e22e6d9524cc2ad2a6e5f4fd6e4ca0f76b4b0416ca619886309582293266e1651322225498d8808b0728cb0dcc160ac6dd3b17b1429a09f97db282f7a415ca859d1c6588cbfa3d9de55be79da841eebc8fcf58335b99590efa8d9504da32afb0101a98f57e61bf5db98dc3feb9a368d6cf7453807a28651a67a9ea79bf1489f60aa60b20cb7e1c4adb0daecf3ad85857e510ba5874855b8d137445eed17f4c4e56aa0c182e5d6dbac796230485855f5d0694fd09f4f57f10b1bcc07578ed90ccfbea37e2a758fb1685f0935df06568bc22cf28de117a1d70e83cbf77225ccb3efdf7cd2e23ebca54f5f83ff069f28ea51a038e1dd4c78c15e3b2fb3d70e907626fcfa6ba26f5f193d8ba0858c79fcd2e89bb7258d3423cb3e08800f91bc4f7500dba8ce07eb545218697385d81f1a19fc7a905e271fbcf6eb3c71f43d8deafab58b843ebe803d6aead71ff3bf75e3f2a3d3d792cb9dc048bf49ef5b486c9e37bf09af48fd2fc13d7e923bef3e2a9fd91cdd1bedbb18a8ef06e5bddb64dcd36d72230e0c3f9b2f50cafff35a59ae7662c0349a6c4d52796c646c7c50347b074e6d7adcd740dd6e74a9e11b4af6a38de99fdb5b1e28dfb6d45bbfb523fd627369d376d77a5a29c94a6017893f3b932496450037d87923775e120a089eec4d4a1226ea10632c996fd2f9c41660a6e6a89d0212fb7d509de70d60a1e71949159b39c9e00bb6ecb79d6646507ea617c2ef8faa34507ce76cb4d640c5327e4a36847a85cfbd7e452be1571fb4b5913a9c5c0032dbee607ec48d25de1d93da5c31ea689046a7837ea045603461189e6352b73ecfe660958a1295cd556e6d94aebfdc0c4e2435ce4efe2a4528722c8ba32609bcf5a0276eb11d65b9a10f2d271f3aa1197dbac21f6df75769dd0e80679bc0e76841fff9affffa0f7cf96b7cf9eb41ff2815dfd195dc188671f02934dfd18cf02e0ff5ad96fc25ed3f0cbbf648fa6964bb83afa1c6294336192ce633275b88317180e64283eb8917cf9df1f47838c373257128d43817ea080121ea70ce25a112a6e80c5daed5bd27436ca802013916e5c89d7195566d3add453f7f6eaa843fdc22fae4e83cd17e104efba45bfdd40d474e1402e0a9229174c857f8834dfab4028bc26b7fd71bfeae47d4eb91ef933cfb40f981b722f5dbea341dc65d18ac560ea61734180d0a286e423586ce122942003b21ad193c3247f36c71ca397bb06034691156208cf1e9ce6fea9a2b451131ddb50eabda66c1d69129163357314e5ddc4597559416e14f0b02ea5571e19ef0452a77dd1696fdd7e1de299ebb312310ac4e18bfd9e8b87bc4e60b9a57e95ed9feed8b3de34bb579771ae4f627caf83bab4c0f69dca85e44716b77ade4261a5346770bfc109ed91d52f2c24aae65a1de18523c538e2ce220c66843e3a685890c4743a28e55b404c4de28974f1e96e1fc60851d37cd8854d01538a866fc68922ffaf9ccd428f26f19fffffd035e42580490992a5a7e95da3b67410f41de11bec8f2aedb7a0c7e9d562a64a58ab542f9183663fab41df98ebe109020cc3b64fe8545a01ae9d0c986efdd43fde2021e285f19fbea778b079849b9b50677b3b5bfcce665b1f0c839b98bf7ca84eb50be3e528adc1e668ef53e6f80e8b1397e910586779d614bad836fc7a86d81a6597e7ec41b75921f9673f5885a8c77287121e754721a4e9acdbe1aefd9b567edfc6c014d1b769e6f854417bd2843e519174c92a35359a774a50101479e4f5d0c481fd8204af68344fa41473e50be08e5bedf0d3472b90ff21a39c9360e4ca44d29ecb374596219349b7740affb789f16c5efcb51f5f14fdfd1fde2aa8d1b1975f13f9b21443bb0a58278c0c5d322d68dd1980a8f4c20f72a88d0bef82b7bfe76f17c324bbc70b75d3664e49d1b0c7e8836e9702ffdfe86f7f1538fefed2ef8a77700c3e7272d5410d2e9b20a66c680733376e5b1815fd2c660ba4e995ab338f90fc4e6bd1c8ebb6fbd469dff5e0dea5974f0133c7677d17daf0a057f3b42de886a6b44767c3ea0855a5478c305d08a29620e59b3f37f8d3aef64f7b83897fd00f927018c1f4bcf7e0f78dc2f90f78eeed7026f21803a86f23287a20062614cedb684177094359aeabb5420973e681bb2320ea627455b6983f2289ec7f37c521598cbe0d978b2cc2165ce097c52375e2c9e973093845900c239bf25bbdc6d3e7eab67347f3624e78eee97442ebdae401628c32012388072d9db353baa46f96c5653096d72a292d428956f9d833a4f2b9c51f192d81983858fa7d389b5999e42a472a9339c01192a80a2440b34a3d5f5a412e22e5aff13f4d59f5363ee097fc9e40be3ebf7741aa4a63c14df0ab001860346678ec8743a83f79cdaab46a393ddc502ff0088f711cff0eb79df7792fcfd74deff7d77de77982846bd581909ea4dd72e8207c9bce1f7044997e96906181cca54f539da123217cda7492ac951a8d0cddca1b785461c4fa96462a492a606556de3229dc3ba3e98ed1a11e9e73f7a3c5c9cbb1cc7c7089422bc2ae5aaf3991aff64f1b795ec0e47f79541ff61dd7e84d4bf31ed7c88d532f2a80a6f27dfef803d507ba2f71cd3b72ffb1ae6b75f69efc0bfd6acb64177c995e2688a4229d9ec8a99acd6874ad012fb353cf89f193b231b2a6ad666f3fe75cbabf959ec1f71407f0e8af38eee97302fbd2e809ce0d046a975bda5e303b1b11b40003de564efcd5c0747af636efee92ef2b3347e4463ff3747cddf9f50ec0ffdaec70d1e98a370bd9944b4841efddd51b7e72e0c1e8e72acece85c088b8891c486a6b23c13394bc0616cb21eace4edc899d4957738cf39b50a714da5034b6d6a1774c74755ea197bfb7626c23fcf44f0c997d66d9ff9eb9758f1482dded51bed074b7421d80e5361760124ba8c0e2b8155319a04139d4d980d18887612f88c8e7bccf2b4afe6a70a4d6dac501bc2e435c4b71546a43712bc4439d04e69c283cf823c1e19a863659bb9474a6b02daac769d948170683aa193d9c6fb428cbdd6f017dd8b28be7a6d24ceaf6b78c952e7d3725e5938086ee1e010897e9d38d05e117aad61c3189a4e7a4d80f92190260a87b6a1e8c61f8678fb227b15c547a79b4ee445844e8eb4dd81df07821187fe66469bd3c9dc033a441046a1f667c3222f045b26b48e0190229929312c8a0b21e3988c26427a8662082c29c4ebd0f25f06b24a95b8b55dfcf576088dfc27ac9d7ee3d7d26cf96e5bedf9fdebc84dc37db4ac0051a798c61032a74085288f4e889cbf4e02fe85f517d08faf25101bef519dc15e396c579217fedbc610fc3d87adc59a8be4438368902051634e016ba75654e3688356b19480d31190236fe5652a9a407ea18a052aa825bb8746313286808c5efa3e18faf82a9c356b229a2a8c3613a5ea9d33f84904efd478a4dfac8f5be53d0e874837a8259926b63bfdd800a3a45c95645e8413e64c5784a364cc7e111f739b9f95f3739da3e0d247c713f930d7d78354ae7c1ddb4d4c8f4856989e0fec2698ee0e1ab1d9c987c1a19fcaee64c32af3fffa8e45f224b136f0ffa75063bccf49f849b695dfad336ca9fdea03f26a63bf1a01bcb89d82d96c54d1052a4cad350af55a3f6de2b173ad2ffb0d12d454b27ce81ba51166766e5c137dfffb5b0ccc47d9dd6bfdd9a7b8a11ef56bef3eafa254bfec6dbfb956a2f45d6c02dc4fb54c95cbc8a44a8b33d8616697dc94557633cd171c3c166a8e06e6f80049a8bda7a194755eafc13974daad84e5bc18e1b81022c9b2a62583b49aa064b92d1106a92630f3f3de3a80c7203f0f76b9bffdf7a5bf9f92fb35df196a459a7d56a07e8e72f39d61163b61687c42c13ce57f3dd679b916b11e3d8756c54325cc9c8f1093673020ad72f4dcfeab573af885ec670ec43bd3e7738e49f751ff220f0cef3a2d325f57f89f2942ea0a9e034212b34b60e966862b1588cbccc9f41c8ec16ad5f86ec52e50e4385baa1e3a00a7677ab95dcde98adfa5b2c483b3941c4cc73e48089852397b3cf5b01fcaf27d3efd3fefc6e09d989e47e61f20257d920786779d21d25d4c428159fbe6b4c456981522c9260477313c274a4d9ef3c7643af069bec9437255e2b0aee33c0429f3c5180159d82d0c1e02f646b00c57a4592fa608af1b0873b6e6e01fa85ef830bffe7aacc0f29f2e66e6d4b19c6b9ed89f7393df88b61bd2b5d9d5496e37d3a38f3a4922302b9c4e3863e003d6015f0cbcb5eca654a8eb081b805b739a67d47a3282f185e7e9eac40730889a48f3260a4980a5b33238ce0754a92d63dfd94efb5d6e8bd41fc64afa19b53f7e7bed7c8ab9fc7388ee0f942fd2bcef7744751776e71a9066dba32700ab125708cb7509c8c6665d6e1d595bbad17fa785f73caa6e54af1c5ddb5d8fad13eec3dcb892c5ed780420d06970805024f24cdac5d739a4cda0ed5a1ed43b07f6e86c20cd97982815c0121e11fbfd5e563971ecb8ce69d0d419112794bdb2476422f62c8364d486a6bcb679fa9aa3fff5b2a6eb1528ffaf9f6a2545d9300fe29fcae2f45a9437aa57a95fdb5d9625382c06a7d134339484ae3520d303c0d746c7238c52610710a4afa22b7faefac8074d60786b75ab3c1242686d95ea2a89ced2be9aae4d61206627a51a541d0a3ec543f3b291fd0824d187930faa2d2f1fed6edcb0d819a355e71453949b8df2fc3c75e015b94b36eb0e21304f0506de5db1fb30f445b8e5e9abdb5eb63b2cf1f8bc676d793b9b589bc99a1549c89dc10783302933258930de45d9a8019111652427dfc714cfa6acb9cd8b40e64f517fe115f8a494e7ce6a20af10db5cac0d96b568b18b092d1efa4ef0d69506f5daec3e68b6a2685b2fcac7bf94c2ce16386fa7a5c7e900b777e6a6683201374219a943682dc576b6982a33480eb81d7fe2348c138bac4295f810eb940bbbd178b655519108531acdf9305b652ba3297a422cc41fe98d3f46dcdd55f9fd73f1b837a257f15dab2077c5381b805005f0d1603ca74d6b654fa9d821b54d74e65673c39207948301e7517ed878dba09e9f768d7adc1e4cd1259455889f105ac3f4a9b05b31ccd1624b68812dabc23ff4ace5f539af7e91e04fd51afa2458c51f251ae28fba0c9d12a952f4bc9eccb63bdb65575bdec7d71abd18fb0e00585e2a89a64aa2a671c47d548fb8857ff65d5a9cafa0098b3a6b65b6929a533011b60c9b97aa885192a78a0c106fa82ec69ef8872cdabe9bd13585b6fdb7ebf6636fe3511492eba5e22ecca5a7731ecc8e67fa2ca0524f9ed43599b3b05803e3f0042b735f29a0e3da868353ac15b4b7803071ef6d00589cce58c6cbcf13e2c44cc2edbe4bb45aac78d10f870bfecfd957bce8c2bde245ed918277886570468ba32a98da5ae1cb0135c74f7c8a21ca199bd392b6913d4268369485eda2bc04b6cae00c0873989f456774eeec0c260d410a305078399280455c81b8bb607821ec34f8aff0f5fe9ce7ed3bf95630cf0fbba1ac8053b23aa9533d7267536047b1ebe571bb562197a97af973b546bb154bfcefd15314d4f7f25b1d0ae0dc851afd9f6f420e7e4ed2ee130cf945f643a8c12d55bb4318248cf084cb9ef8454680488a21d15264cbc57ab0f47a3988be92d06fa69e779230c27c18447af1f676d353a3bba77c95c757bf8b66d7fa78b726e216cc3670a831ad0567591b2f15e7406ce001112b6768cd38d2d9e34a67c6a20a19ecc6c4e28c525bf13cf2e7bb9888257c342fcb49b1999079333745800a60be2740a7e2fb99931bafaaf3773080ddfff7f7a17f7d0d3b9fd481e157fb8586f056d0b9187a5541c8540215677f4a2ec79cb6097137a54b9a98300eced6e44e80edd3c95d69aea77193042e67cb4179d22cf050d4cb03291b38ea11c7135d4d6ad259cda34927ebd7bb69991943250bc1b76e8f3ec53cbfc85e27e447a72d2cdb019be798b29191fb676c0ad0893bdd90908954d3e3a9cc4ec74dc809697ddc989c6ccfa519432ac04ea4c6312cc60374a01ce113bb81d75b0c604e414d2eabca7149ac298abe1569b210fca984c7cd36a2186fa291b4d4b828911f71952ff2a68d32fedf6a931b591eddf24cbe9767533ddd84fefa0a26be3765bea888f8c6c5791d09377b9b5c31eebb037d90fd1ced4be78551f8151ca12b817bbfb409405d70b5cd066b63d920fec22c5ed789ffcd4692a6513afcb04e7eb3cdb73febfa0b5523cf8df419d4fd27c1bdcf00037b418ddc88de8456a47e1ba6f4ab790498393a9f3ba72de7cfc9e9d1111a3983977020021d02f96325d314ff872379d4f348be916dd9b975da28fedfcbcf41ac2f45937913000e3de60f50b32668dde0ccba8bbde75b72fe3b24b93e33fb8176cbdac3931651ee7750ef8d5d6971522c85099d1302b9c9369ba21a8c38ae03d87bfb46dd4943e56d70423f03dd3de11b671fdd6e66ba33c91ef8e3ca989d62ded94eceea7cdc149c3c9f19af97ee77b68cdac9f27721333d6ff05f746f4c5d7b5deef1e0f06ca183c427c64ded1ca4c494a99d010615c5829cd09127a70d3e8bfce2ad69a25f2cd033f11b77778fbac505d1a3e2a02c76c71d3bc08d0c49093e56423bd724b04375e7db2b9d3073f4ff81f9f849fa8ebdeb836e7372e7ef253847a87d35d22885b027e120c3170702a03bed8d9fc99e7f2e06e846f4c6906734dda280f65c93e7124f4702c9c79b39470aa44c6ec3d16cd47579ddbc49efc608edc74c4bf6c6cef5346da9fdaed8cd0f01c34394ba4897bba9491c386c46529bca5fe1136ae7c1fb10631c6f3d0e697952c04c6e408395bc3b965e3ca1f6010cf09b1db5a249a03eaddc73535bf07a2a32e72e85c4da9f7a43077c0fd3dd07fde291f44d2c9f0fda13f057cc51189a4e8dc9d4141cfc888e0e7ba4e1d0399004e6b88ba7a07d6113ff6153e88dea274b4ddcd918ca432721d34ff4c29ac0c5d8405893555d1d5b90645cb3e15e45dcdde8909299bb3b144769b14af28a0fc5643e9a82ee29b5821580f92e4805c7e5b41173d0aae79ed8b33a72fc0530f4de127ad3afdf890fec312f5a9a17e1b58de1954c879c7ec0f1916d969e622c985b7070d89b2136864205e20720502ec8d91c011c7e094bc4762d14b2703a6ef676c365d6080d661639e57504e3ebb375ca317660caa6cb6df29e57f1177717e8d5e52650b4cf2f3c427738b111e8203afaeb350cd1b7bbcbf3176ce576fb82fe1edfc1cf3f0e9f919a511a28a1660cc316dcf40ffa531e695fc6f3f14917ff0a3894ab70346624d4e3b1691d6f17a97bda1cce3a1e861d4085e3f77a6dcfb5fda1cc5e34d84e6a11a21cb251464d01933dc5bb2940aaf52e5fc1074b7a6d817cfaf9a1e7fce4f5fbe7b58aae245b162e8df640fda54a1138d4fc636a6d0716b84c243e2243cd732332e7e1c1a2cb817a7dd1ef2ecc1e1ac203e92fa6eedd98bf6777f39208a3862b6ea771baca241b9623cbb16912787dc7f875d7fc18b39f3d489e357c1f37d0736e5e695ec4706d753d73000ac126b217197b0849bc7aeb22ec94395a34be760ac0c634f9008f4ec880c344c599cda6e62641f0d312d00f8642ad55423cae8e48088ce07d95eee0a5212e69df2ffb05189a4ea80f8bf8d763c72fd216f3e58d00b17e02bc51bd88f0d61e62dd8438e1ec29a39f0fb0310669d51301494c792a2fbc24d9446b647002c682412a9be2200e6062c51ab4958dadd93cc9d2d5e2789246a690d6003066e8d11a9c09f3ca5fd251270d2d4af38bd87e8cdc807a48e393ee451c9f9deb6da24b40766d8c03be21cfbc8e10ec69258f23f7c4e1f480420c027312e8c42cd833bc20ad4329cf90ccd9293a946fa2688f34c87ed1ec6d0dae8f49d97880b19bc055112958cf80ec1b00f235b5ee09025c2dacbf5e869b7e55cc18fd8dfc5dff331c822bd5777674e86ff4a13af53f411f500babc51c500bab455c26bad9cfd51946f9a9be525621ab4ed8014845b95c26a74c850dcd472a079d9a0e3a5df182b68baa18d5ca2cb646ae740e397e4dba01406113dda2a74763c21eb73b6d3dcf16db7f5f43a8ad1af2086cfa9fb7d804c14fbe89e700ae7f50f6e0b3d448276f04383c5a4e919ad5a40caa78a2136630a7c7ae411e7ddceaed4f8832a71e6ab6922a5a6ea443cd57b2f770833d4d8dafdfd12eef979f7433424e8c7402b8b942d451a24e36830473a740695a64d245438bb25ccbdef179d97bc11e4ace95e895b14b6b7825d42196275349541284eda99cd6f97eb22a44726518b3f19ece023133a3b358cf56ac68c25b0459134e01395b5259274dc4e8fc0ae4c64536958ada3fd2936584a727764f8e7b46b5fe106b7fad76fb1184f2d7ff7ac4a4bf7c5ec4970d3b1b6a91ff118f8ffee3a2b8cf34deadbbef655f3b077b3fbc02183e3d18a2dfee076fc72d3140d70cc0ad85ed126077b0518982c6162b710c75ccc4453d0337cb1a00a49d4f124e85c9a7fddc3ded5c7987d573859aa3ff5ff6dea34979a45914fe2b6fccf2e3303248425adc1b214018810c4e485abc37e4bd7788c5fcf62f30dd0d34ea56eb79e63d274eccaae53a8bcacccaca4aeb2c7512a772767ac036a0e5868218f7aaf52f4b375bc9faa6af589ff5997fb508f97e635e4df19b2a6b0fbbe9cd7780ef16c9f9f6ba52be5f2891981cc100cb55841dd0333d3098ad063804b48c6355caa6f2529f8347864b96b800a5b47f4a97fe4e8fbc131e9df6e24a30dc5adfb058012f74fd186b8ebb71e169d22d68ebfcd38bd0c9fb5a1496469a37553cfe10357ffc7bf85c66fb0d0799e11b5a1ea557f3dcc7e11aeca7dae309fded3f2e39e1f79f0f2eceb3af297ae6efa0b16738d2a94ac233f07bcade1e5d3c8b2dfcc8877a3d25f5891c53c7b53d805d700f9ac6c1c964500fb89cf7112491027ae925c4717a9006710f49c2cd693c37d8d4333676c5cf4f84ba1e041108d482a7c47e0d326c37ddede632cade026f3e3b933f84dce742d64a8373f999175ebd7c45d9e613c93ba62f3cd8ffb22601f26babf66188072adfbfb8d0bac54a5e3a991e41f570b8dc8f413a08414edc03638ea3d513836b8ba0ac20799a8dac91c5582ae4ace0819c012b71630cd21e2527cca09c494b7d0d4bb4b046058f5ef44e14dd31fcf2efa28aee649a92ea7d2d0a825bd9f326ba7470bf340c724799e75717dab448b2d9d045cec228e1141b73b0380c6772bd62d06d85cc563b6e8e4f834c1c85254eaac04e833d64bb4112554026ac5ad712d2dbbad10083b7f52438c093f5d01f6dc7761a7b1dadc19f68d30ae37a11fb8ea6e45fae855fc7f9c7302fb0fef1b22dde65766d6e208b674947946d8b62a72665f95362106b4e051e8e8c471b4250e1c6b447cb2395990cdd181b63c3594293a220f3f0e8a072a77c69aa991e3031e0e11a16fdae35d10aef4610e74da6cf5f133f0f23bcc0f6e5795be13322026777524fa13d7177a65b3ab1398f12f5b0ac7490717b80b9423368cd9d40ac2a4cab70fc9e681b1122c65ca8216cb29446633649a893c597b0c28f16f42a3a82dd369a8e888e4a234d1d5d37bedacc7f1ddb1fc3bc40f9c7cbb6785f4812ac5293c9d08244065a4ca86cbf4300c0a1a4f1910c78349a6ca78cb00768a1dc0d2dbbe72366e06ace2a995995ab1fe2f54257c1b16dc08208826b4edda23d6df71f112c7ea49ce568683ad6ef8d40ff0cff0ed3774fdb46a70b283d5a9d2a04c446237cbf3033f2b05dc2cb6db5a0d13aa216c254452cc11335075ceb4b8b84117715cc65c0b20d606689a04f83c0042af64bc870ed1325110833b63b6ac8511638ef387ba14239c1f9dcd5d72afd85e1bac561f17180df9f3d7d071fe8dfdffd247f1a33d072a2a7a23ce9e56809aef0715aa4bc51ab16320dccde74bc958ee25212c864b5cfebf1562dea8402514207bd496eefd8cd6929b8abe19ab1d898dafb5924cd07ceaf9f119df33a4e2f878e8f2e2a8f9d33ae6d806a25f06f0a0ff184c7e710c5cf24beff117d338d82feb551c99d7bb1c3c9f4bc2c1aedce1dbd180fa09f96a0d1daa7a1ad0ed1849250ae9a6ba43b09bd7ab33e91a299c16eba74c59803e6eba9a6a12a5a00e95051902154e9c1d80e2a332c8747b8c011d05172a072407f8d86eac6da7afb6ecee0db0f2f72c7ff28bbf5f3d3c8a3e07b91b09a69b611280fe3fc70193f41685ac7cfa7a91f587cee0600fa0fb72f02ee1b89ebc043c6d190655432801fceb21aafeccd786e6b43c1d168c525dd5d2a5910b4244ea62c86d222473c74daf364af72c3d45e9e568e06cc577bc485a0c0cd4ef00c18a2d6afa7f8bbe59dadedbf3ebd7c8e997df98d67d45594ea6f41fccd357bbf5c9881915a46df8f42cb56c2a63263c8b5be63d715fa38c6dd4a7d7c7155495ae8248a7f044e41648825edc6c315121c29c6b571ff28f893a90859c9617e5294b551558cec08f62a42e01d35f6978a31573726460f5d1c96397d47e320bec95950e6e4aaa31bf2dd66d20f95e01aa00e3e57a6f995c3eafdaacd6bdfb015cdbb0f956f41dbb4f0bf3c5375b71fddc1ff44d4cbd3b656a4fdd018a4d0562da51307d2e960bf034ecb916049526d93275071f272b6903869898ef9c0631dbb77986cb32d9a977c4ae312b3967c8a293379e12441389cebd3bd6250dd34a016562425748acce82bb1f39a505a96854a185da4553f53820b598db0015c2bee78656cfc346e6984faa3d4f881446f1ea939c507eadab7ebf558773cf4f4e65249a25557af1da29bdb32637c5f23447e27ae47b302f1c72136dbffaad0d6a3fc5216f346c6e7c95f7e8e6e1c8decad283df4146a75f9a6089de48f4fb910fffa89c47642c7acfb6614b6301e7637987c1ee67e8d7f7ad9d660e2e268c4615cb8d91327de04911e2f97d04a5a485c9c1e026c4162274ff4525aac37a531a18ab565cdb0bc8c6adc4971b97257c7553133596f2c53ebd9f0a484bdb99affaef3e46f3222deb063a58aeefc6d46c4e7413e53e7fd555bda244bdeac7c8ed5351320439d87f6f896068ad382cea36aee33cad68996274325c5ed0c71e8dcf60f63c2d91e4ee15a131113c3acc5414490cd96c41168be9bbb836ad9ebecaaf990a19691f795d42a82f746949fcfa56fdb7096df56e87fce927f43f7254bf66fa5f57584cf84be3e6f4be5d9d038d427ea081e57085039a3117da4b913829fc8a5001f660347e612c1e68d1c202cc33003803fa2450499277f5ef4269cbbeb05201338a781ebad4fc2dcc0aa23d9548be31b2a2b7e6c2b6111f4b3286de847df623ffe21abfc2675ec2cc8b3174690af58e46d5bfb5bb9e47d90cf8cf2feaa2daf4cd02de4088eef8a2397f04c7f1d2152911f423aa8025c2817d91e9a19223d7366848b8f171671ca9032e2197d3dd9078ba391ef391b0305c7cfd8d3d821a73b2e29067f1baffc9a13af59c7fa478dfadfa8465db2f7b3be71bc5431ef5f2d9bcdc5a5e15fb0cd378c75bf405f7f7129f0d2e2448cb93921cad5deae82cdde5681b157d3120883bca4f07436aef3c1de5595438489943a0708641e9418c30fcd193c46d9c91e1869f3533d938e6c3e2f99de6a6712f354fa75ef08f6d3da9a9f83bc9e097106090f7e65bddc61fd0aac9d29298b380fb2317a20303b39170c212e211498c7ba0f6ba39538189315b223552fded10ecb7b84c403a7833681d849ef48e5655d14e5f2a41e7d67c72dead2db1a5b1a537edd94f418fcf5699d7c130076fde67310d8b514f3eb55756f92681bfa853e34e7fd09d1be0bfd423f35e46da4e0da15dc4a17173902e2ccc22193b18844fb6566ed612a2167a70ce6632f4094b5a4ba414fa60e947640b6e694dda548c1b072a080468d98a86fad526f042a22b866a25faff7f97b42bfde04c8a5f4625fadfbba612a85dfdc44a853f1a96f067b21d09e3f6953b20aea1384c570d251e176bdd0f73641b9f1d7e310b2ab99d7d54af7be17e6916784cec9786b76366cd615fe915fffc8af7fe4d73714fcbdf22bd3a2c62cdb8e75c25e8df042525d9eb7ac1656c45eb03fa23d3177e23a1fab43d41292d9be403b5554f9473cfd239edeaffe114fff93c5d3371dc4ba54047f3dc40b01f5de53ac458df021df8ba1de6cbb97389a18a76b6c42d469b639604e2709e59c7f476ac4bea2194fad01bf8850f8473efd239ffe914fdf50f0f7c8a7304a836b16b2662b6966349dfb7e2dc6f4d328778be9d3bbb611a616238ed3d43fd8d80c1eee417b3a28d97233a8f00594c115375aac4a121f3aaca8f0e5968dd042261929936743d11def4dc8108e056fd62b64470f7bbded4c5e6904d9d1c6fcc308d38f29eb4e16fb4afd77fa639b067b4982c74fda5afb0753d8d53285390c7b42be61c8c4040601a2f1539b9f0ca8e9c66233082f7adec0dfd1b42f18431d9478aea7865512c1470b979c4d3c11e9055ce0245825f491c9d6c56ff2ff054aaed9bfdfa1d392be719439b913857f3769dfc77949d5f7b7addd37be390008f2181fa91e3b854cfa284d0ccb72d043250c50469c4dc330d2341e2092493e4d77642a831370d373969a851eb2bd8e877b8959f8782d4ec6b996ee703ad53b16baf9a197ee3f9f90f581e7d4880d25ef5f02aefe6682df0ff592e6f71fb4f6e3978ea54b12136f8b689a0d161960a2e15cdfac681658ecc8a15d2ca2719d24144b1b045ff2c1ca66574c315cef5c5ecf43c1c4384d9238743b8fc223d233559a82fe437efcff61ebfe3df4f9efe481eb202fa97f7dd53a092ca536914e0c276195240871604f781261589c815925c993ad8aa50c3af45898ee29514d91b429632a7304b3233025760b4a048fb9cdcfc681513b4a00b8a2638246b7e5fedfb97c73273823edad01f1df2db39f877b49c8e78fda9254d12a5a225043e787186cf47a2048d909b14232794c691c5f2bf32a52a69cbda80ee389e97898542699c41e35cf3030949eaf96a782dd6e03523f5613785ce8908c22bf4982fff72ecd2274b4a8b13ee6efd2736fa3bca4e9ed5d5b3d37d2217c9fc0d5580a7ba50c2463ce28f272ee8e50374087bbe8b85118832d4c303c08738527e6858262eb9a9962660620d3ed3e141615381ef1d91e5c22ab70c45752d54dcffd3ea8e63f41c0c662cfbf8d78d7cacfaf9eb7255a8c468232457a8b79550c8598514260bb8d83583a6a34937bb338964c5050f5393ac26c1697d840a8e9552111d360b5de9da481a95828b5ddefe79384e9d986a3837ba1dbfabbaf427bc5dd8bd48e67f4be085cfa0f90b6b29ddcc862a5b1c1f5ef92bd1f03bd24f4c7ebb6f236e7f0f5c24ab3c52026d01437eb6429f40a73e96fe736613374428e13b584452f2ccc849ad89352b50a71361a25c78a08b375241e4c30e192383e394b7a396726c8ac6369c8bf8d4e51aa1ba9a17f7758edd25dfff51077b4797cd147da75e087cd31a7dae85e098fe5f6688869becafd4ad8d9a3721e62cc5a2b102f6377709e29023cb586d8061e0b047e1856ecd6177b28104f2baa5626d572b58a2861586c6a95ec9cf0f11f8b36fc928aa9a1179ad1774227771a9b7dfe5a7ec7e31077547c7cd136cbc35a1e820daaec0b7febaa27ce9c10a44f3bf3f16c8e9194b0521890147acad6898efb890e6189c843fcd8c6750818cd8432d60f21ee017367390cb19d6b44220b4d87936e6beb376479fc40c36d43c7f77e457f8fbafa6994cfd4fc78d756585294e058d01a5ed308b58a69919137b890a393f5110782a8a7cf0edbcdb03c888916a9b0b55ed5ebd9793692365b923bb15c9f183ec4265b162a77ecb09e1e9d405f901d6b60fd4039fdcf9f4d9aa3419b9b21ff7d51a09f5b24bfa46ed53bad2894b62d6ce59cf05ebae60e1459e5955727b1190699138aae68619a3494161c2bcbb955526009736061af4719181b6c14707475d82c738c1e993b7da70fb8ce560423d35e6b3cdf448b36448a36d0a9b49adab5fddaf23b03bea74869456d17d918ab6a371a08d53400d7d6a8ee4d8ba13596386f1c58c1d8b76cae17bbdab14cb5d132779639588ca73dbb54190e32eb121d318b60b324c02da3f6f46a1f9aea4cefb9ddf63ee7f2cb7fbf5679ef52ba52e03981ed25b5ce942d8c5641f5dd4f13cf83dc51f1f955db33458f56424149eb5544ac2bd95da6016d790b641ae2db91b5e1b083b81ed1ce70e99002ef0747c71ba1f44943a8526476f01c040270e3a6821010e98cb5655d0f007c1bff6d41f55f12f64749104fd47f5d0960f00bb4ba1fe08e4ef78ffb97115a9cfb165b0ac34855f062c49cac87c27c546d58f144924bdaad10a11a868705d05b0fcac2af875bd6b424b10c30075d58d3da9fcf9668c603c739a98e862c42705bc43ab075ab8e91a9e117bad1f79b830c3a29e6ef60cf8879bfb9843b7ddbf1632133894013c6be04e4646c421a375aa14e22c27e9be641a991e7ce17358721ac8b8a7a837a9dcde5b27f05f53d6dbd1098edb12db391f8e35ef158d18e799032f41db00c14fe948273d45f6493c84a6b4f558e5ccc99e4d4f450911c4ee494ae19b2842a9f1106635e93d362260ac25e6e5580f9fc4beb6b72ff1715f43b2c803bc06f18b9ddb6ebd82882bd79cd7bc6a1e04f4564a43b5eb4b86324e2caeb9e2edf8895d4087523f5ee1aa63f6ec9b9e35fbaacff05fe8934c88cd4299b3b0f9fc56c87dea9579817045dae2e556b5b7450154aa76452b5d4e170e939b3019eb8fbd5c9d77a887044aa00a8ec0511af2703544ac90d1bda47c3d978f996b3b785b7594d59ff6461a314d068dec8511a282022058256ddabd348331a6bde5e8a25401d34c62bd40b1e2e57fd1ba46fabfb9f16d5245a8f241576d138d2e7f22c8456d5019716ade7d20f8d63ee68def9dcdc34b18e75995e0c7037c7fbc76d2b333139358676090b527b3434625ba19194cc77f63c1fe398ceb244322e283bcfec3216461e7940656e3822b72cb1a1c2296144e24cdb146a2f9e164b2ae6e6546fae524c3b4911596933e5e16ec7fa1bd02b522e971744b4109943de99e72bdf47577ab85c301504828b1e7ea4c50d6a1de0757590bc1de6d0a502aaa293cb4b05109974abea8135486131541cfd44ac8b4007055e6152d99f120291b5ea217c6bd0e084a6ef5876733a43271def09f815310f8fdaf5fb0a282dd256b863ed5678a9db1115173995f1e4d268d3122b8d8e755fd1f5e63da153ff8e77b0d769dd6edaf4f138531c355227826dc28dd1f17c5669b51a1c03219907e178241b46e59b34181bc7defe305c87f590e3647197cf516c3763eb9173f22c87ab6c3a6796a9709aec8ca5566fe5aa9bb1c68cd24a49f5eb8cff029f3be438f1795eb71e366779dea486a65ff53de8c23d57cca6ed780404aacd161bc5beb2318e91076ca9533a175d8898b7e9ef9219851e7dd54bbed3e9f40dea791e6fd7ed9ac0e1d3a5e32a1a27499e38ac926505560aaa28633b6dc3f159739b4fe8cf4107526417eb64e6f7af005a7432c1e21ce98d7bbb5e25ef1d63b9c160908b4f2cb0f24376591cdc1a72a6ab35b09f0d503a99fb1b0027181ed2094cdf9b9e894d4263b02b56eb1327515266d3bdf9110f5a49f642f51dad6f845a5ac75f45e97590ef0fa0cf087978d0d64c7b1262058f96f2f684aa3b66371dfb1a64aeb9b0aea953eca3d12976a0112a69e528d5239b71e7706a66a3bc16b5822d2a379b2cc46024f9539845a73e2dd91b98c8f18ec578c28fc6c0506363e034531a6cb8df35067ee8cd7c2bdf7dff3e55423d0a2ead835f6bb2af3a03c30de2a7089a566fc7fa77457059b84510b7ad75b7aae4ba170fb6d30ce2eb5c075c49cb912a2e1162ee48e3442736be86ca82ee81420f974e25e66ed091ef0d769b805f41668e11bb230f2e187946f70efc740f8fa676d5cd9664847a3f32ef9bdc7fa24e146ac6ed4d739df422f8a27517f427da45b05f61de707be6b00b9c161efeb12f2f06c37959ec69fab85d2ed331345465625c80b26453e48ccff87d4d2c7d661f9484be14e16838dcec8364680e44777154ab7a564927f5b48656f032c3b345aae8dd568e5ec4be71fce8cb873d1b4d6d2375f20fae7ee8777d63d50ee53d9f98fc991ecfeba71bb37f6ed6d54891b4e01976e3b10366ce43da81d608ee90f676c071c283ae2d4ae395496ba3424411db3617a3505907e49e3127da0c9930e89e398cea914a4b18b1dec6763070f3c972e1fd7a2f9bef99ff8b05f0af3661e145587f111dd3d1ca70037aa1c3f5b28d75e14c0771936d5cddecb968e0f76a009b2de7bcaca653008f61c952409acff564b79060d140239be62744a64c687ac59731d163b7a338154aa3d445b9564317d64a9ca374b0cdf921f9ad922101fa40d2461a407dd35d0d0e208550f17cbdb1e7c7f2b85e071939dbeb5a8b5fdd74f0c31eba52b5fed9e7235f92f52fffdda2b50d2a932202ac8e7430ed0d2b7688c4fb09969036015461a9af35372176b26b94a578daeea3a16a061501654ac2b8a7d9163e260404443a0beb07e0c0f9c3643d6727b356c42a8cb4fe32e412ec94b67e07f78c888fbb3ed826193db302cab3b821adf7c613453fa0a0461ee6f676da904bd534a7be910dbe985707767c84fd3437231b5ce6f72dab8699641aebada48b2342d94fa77e09af0d0f3acec316bafcdd88cdfb70b7bea08fa01f6777de955b7405bd646ef94529cb90e4ec5613d4cd12599b04d35a80a96a17d5a23c32e348e0022f6782434fa042db52bce93c543221b631d4e4dc19bf3bb0205b0d7ae25ad91a8c52a760536b8407d43c6a8f4d27fb9f7788be830bf4efef2e3b638b1ed1e3c51ae5a64713a1a3022bb8a541e410c959496cec51531f8224cef2fa7c3b3324311133dec06283ab16a0b6f7717c7390b18c874a9e11064aa9031b4c935120e3a16e67fb570a74c3f9fd3a51d3f19b0f8f5dec241f60df9179beb91c835bd849f0a1afa91bbcc7d8da41db3beb1e61f436d51167ea0dcc0f933087d03adcfb6a30d8ed01d355ad1332cb4a73bb9fec8c201f8f73619ca888ac5153bfe6d99d4da8a3ddaa9b4afdeab4827e775a6946b6f58dafae5b09917bc05784bfdfb62c0d429741be30e89d7e24078cbf641cc2a6e895351190364bb2eaab91de2ca7061dcedd6f402fd3b95ef62f90bee71ec2c81cca2fa1512a478b413a9e145cb11c8f366a41c6b39932e1d7a6bb35fdd951af91a83e25967d2c30cfa73c4f589423d7e84da7e04824b4882af16572c04460187674eebff3cde748a73c8fef2a52437f628fc705478bc2b2ef3b1777cd1fe755f390a1fb4711c6cea5a6c31f5f38f55343d1fbb1f755aec5cf37c837a067cadc2edb38e9a1beef4a3e8be0bd39052deaa95de78a9a99024833ee6beded1bdcdea570299aa758465f5772e5b648078fe6cc6fcaf3b76ab67e9eaca2fac6c709a389db3bec3e8fb06fa8bd7b72e1fd16bb50bece9724715890f6683ae8092b32336a26918d917b00c9391d1db95c4ab67b99a58f4a41a5c14a1502787a80ea1e0d4824a1ef53723c20163bc3f5e5832d2c2aa5f2961d833aa3d4b8868e391716ffeb5314c4fd81f9af4f07662753d254a95fff6b83e3eb0a06fc423eff853e37a6bbea3eff4f37ce67aff4361cf4d9b9dad78d3835b4ab03f5fa931aec2767dadd9ad7be66912e7ae91bd01b6f9c2ffb70bb427610bd07f0a4275853b93e5629dddb0a0b2eb37bcbba3acc623d1bb0bbc4f19c653c61e4c3560b48a95e2eacc4e37ae4a6640fb4291ae2063a1d4f43653e450a645ccfb1b29b09c54a15cd300bbf6f7e9820a10754078e96461f818697ea8a8ff6c34febf0ba2b37980853c33242236df67b4397bcfc9f93e30dec85206f37fd0bb4162499612e2d4c0e690ea64b82f0466e49e5274766c78b3159940bdedb3afbe9627d147810db8c853819e8c1248223072347b1a44c2465b749b1e188b257780a551625cbcb56faf3c74f7d4bb589d32836d2dc69d4a8bbf5f3fe72a447a47d7edfb6ef7786b233cb1e5388936ffdfdb6071ef62e8ecf30cd64042698e36ab4e3d9921c9978a4f760ddde1187897f28f0108452654dd232c32df1b18db95be3a86cb86156e998d25153bc67b43ffe0ddd977b6860c928eda745983bc1173119dd64c5a701ee11fef1b47f1da04d9ed571a1414039f123c49135534c36d2d405cb589cb2d14c5f32e4de31487f2a715800843e6a10cb0da01a3313814e88024d173d656de5ae5d285b7f725c8376e0a5f31ff16b947e04533763ab93aaf96a88277cbd3fef5f0769e1eb75e25d2a4fb7095aee61299a45558514ca46209748554b6364b75d8c1d5535299e4b8b7d814fa7fc904a0570e406ea82a3e270c0011e112ac49ed2a50d3008862c56744c7a798ffbf9e3df673d0d6b66cb633f8cbe8a07f8b99dec1dea0da397eb36fed03312696c3b71f7d6e4b87651b6c8422ed7a7c749c502bdf9a1f636f88c5a555a4e0f033db1b5ba868e94866c37d3f95e2c8fbb18c320491196a46b6e5632a18d932d968703b81b128d636e847a3fb315df8faa776511fea46b5c2679677f6ec675dccf53e3ab70ac6e2bff06f70ddfd7bb36cdc4cf189f571b974b0a8d5873d57267a9c752de69babe45039ff52b8367d60376bac2d534525c663541c7e2844c940c97d0fa48cfd1752ec7a8b389d281345eac74343e0da1b1d55181f49d7e7edee90737ce455f749efda898f44089fa4d21fc37743db17f418566ff7f1703db0de63bf6e336d5ef2e7e448eaff0696ce9eb2961e4074621cd1970d8e4f64217e9991423010dd35608c07eeca9914eb87c08e6c43e6187e471c42d59f3b451a55a302d6d2878480e3071283661fe05168afe59616f3c2976d591de00bfe3e37adb47dae949c66472a07cb5aa168325b88ab071a2ae55129b56563e3e219eb453d23475a0c833bdcd4a26932cdff7468332924763d7c2a73ee624a025a6f40c63ca8db5ca6db3b4b86e02e0e5fefe5fafdebfd6ac5e447aa786e5669611bee6efcbdbbbb87ef0d9e1f836cc455b7e1bacee1b5aa0645aeac41fc1e7488b7fbb4674bff8e766c5faedc7bf965f683796b900bdb2cbe5b20f7e2a47f5ba5043b84857762a8e4534aa000a05181be61003881408700fa0a18c96bb6d3d582963b7c70c46491910c3d96112c3f53e3f2d32644585187adac0b523cda6890a9371786aca84fb8c882fad7a8f94fb112e3e8c7a1f77fd0bbc1618590fa53af256ce7a14057bbad8cf706726949ec5ad8e552d33b887e98304dd92626c2129ba9369d9b196d3728cf1f1a497ac095c1d4fc6a864f8bc410ea8429b44acd1eddce7bee51afd75e58b378eba83d4e4fe7efbd726fff7f3ba698fe00b64a07ffdfb82cd5e2216eaa3c430505df1008cd401c5631b482362a2c217de066cf461bf3f7ded5a4e0d5fc98de6e4fdb3b234ecc04037a817f6b95d5fdc57c3ef6c75025b9ef4de7ebf5ed6f1d83b6df519c9f2c04a7296af6b453fcd2688ca4beea3e33ba1d5cf8c58b9a8d1bf79a36d18e532db8677ed4ad1565ec92d1d0b83f303ce43ae052d68bad8b228cce16d66ff11a7dfe49feaa246dfa05e6677bbbef8a65aa8d1d3acb61647a3742077ebf012610f4b39184df72ac969aa1410fbaa7656ac2e64ab4d06a00783e0549465787526ed77076795a0ab9e71f2059d02495c55190a1d1f761d93a33e72275fc727e951f0d649ff668c7bd2b1ed3cf0af6210bebc1e3cd407bdd9f3e2be12660d39c6ef8912ff061f8ee82d44d119e8bb7edf1c8fd3b5f7c80778a07f77f3221ea4896f870c961b083c91540b75dc6a13a5e1b0360f028bb7904cffd540a5a6893efaaa7e32d10ff040ffeee685afaa61a2cb53b419edc2292be01acc327bc9c5e6e3e3c2a8a7d6af8611a951e4ab4a66bcb43bbf23a7b295fc46ece7d77a14bc37817d8c88b97d10e6765fb30dcdfb6c47fe576359d03ba8cdf4781eac2d3dde8003fdf7cb1701390db418b0f6c271544822a4b5616c997090f7185783adac99e95ad2e22c0c3223752e6e9e8b34f8440c3d0a0cdf088c30cfeb9b3fac458dd57b11d184ce41e7b8ba0ff040ffeee61250da0aa58a6b9bc5624ff85bc8c4a720e724bd3d8d0c66bcf6cb287d10614fc188ff6a1104772b9d77c3f95711e71db6b87bd0977deefec1350cbd45752f7b96807228a0012ae6f28a517b253fc43c8d64d0d05b251ee8c7095944ee7639b3d67988181b0791e55900477e049f96d3792f63b2839270e303a42764c8a5c5106e67aabc5515fc22d60a7a2816fc43ccbc475b3ddcf72f30bf53695c2df5346ebe70d22a974800d886ba63e7318436385f9f66961446d6446df84f1cefa4bf5d805ea673b9ea5f01b588a52393d13675dcd94ad619f020b3d9225cd1b14747de6a088b92bb8fc311e854d9c63ada437d848db9741eee466cc58254cf9f4f48fa540cbd15e94d0c09c44f30408276c75cef2aeb678e15c23755e5a9b1805265c84dcfc19f4ba19cf7bc6b0af85f172bfc936d2b509dd0d0ef9c5c979d02bbffe86a92bc69414f6a9219a54669a47dc5ba35c9fdebb94ef51f1773f69bcffcaf4f3e735b49fba5e23bfa4d73bfba4e3f07319ca7afe445fab663c29f9a489e25b3fe8533d7c9ee66f92915c9cda2b0ff11a09629a671fb354f790381131897b1de3cc1d09f1071ff41a414b97df9bdb7918827a7b2919e71a2849ad10f6f365ef879ced7d4fcbf1e83461bb316ee3ec8a3c2b2fb5a14798e71fb81c8a3c73b2fc2d0f03f48f6c2e253dcd27f07176a3548e8eb6a7acbbafbc2ac0775b4317f1ee06e15df3fbec8ec6f65135f2064961d12abe6b45eb59b6a24ecaac89c1daf5e67717db326fd487f4b4e41fe841ab31c9e7f6fa8e44ed98ca9fbb0afae98ba0ef10257d71717af08fa1db6f6d17430815dab1667c6fec0133c962f16b60802cabe9b59f325bbfcf1290622cb0dc5cfedba7f83f3fafcf6cce5fff7ff5ce3bf1a49e0a4465f77d24bb58faf425dbbf1e903f41be21f9eb54be57767db5c24183639d6a2eab014d033075ea20403b04592f6db98667a5667bfd20eba9a063ec1bf9be9ddd3b6e602d104c383816d32e3e44c99a247c684ea387b3588fd04db1240dc1b564349e4017a34b7d3fd64b2b72ba557926c5da84562ca875d69ec01602209b36c321b6e962ba0a4da7922aebf39509cb06f3abe112a8daef16ef9a52f47b8c3d6c3f37639a83ea089790c2d0c8a30f7419c1d12171828232166a9f6332e424dd16ca3c9660475ca5d7c067e37cfb74797297ea35443fd9d051da0cc0f560463ac2b4adba287642b0bfb85dea99285a6f8be91f66325b7df7dd24f9e8fcbfc2ebcfbc737858bde86e8c7d76a3b4de8eba6a0be43fec0ddf5fe82b86f0d899e0f06a4ce29c78568791acec69a1af86c45d4652b43e2e56736efdddd2675f9ec329d2b92af805a3432c8f629a12428676e2d784bc46cce9384bd82a60245baa5c98d11df38805bd60482385414ef042b8469045b1d5b256455c11b65aae5556d42fe380977f3620aa5c749c796124a6e5fe39bdfb9a331a6e13a49adfaca1cdb198d67b877a8d42abd4d150ca84f2ec30137170e48482258b1a8eb84ade3e4a0ca7ed46d0f7f5a2f3f6eb1fc04e12b9b483793dffd007718bbec4e9f136d1bd09639478c20c283332a177921ef186898d65524d960b3d9ef5b7bc6cb89ff8e9da679c2adf6153a2b7b1e042ac322192ed50a4015ac9ac7fc7eedb4a86df036dcd72ea5eeb37a732abddfb5cb8a5acd58001dd434578289ec02ce2cf116e4cc7546dbd77cff3ca7fc7ca6d08a34fba2d653c7e57c07fa3ab3bb07ed167538183a36609560adee818d142f08c95e1a436b80bd16f6df2cea28346e91812f0a345e0edd7edf383af91f1fa1480d1cdeb42d5e4282ba9c6aaebb617e8d29fae6c072de3a763b5f8a951eb7ea950cc3ce60e4b8f5d7338a1e8c80cd8932f7fe4acb872a3a03ea5c297b159fc6fe38169869519d44d533343a55b1851441ce5405b74439180e51a455ada7d4523ff96b7ec3d27e837a46c3db75bb455d719383331e54885b3ac2720361dc603fdb88bd346da32c5aaaf2ed743a30ff3bd8eb7c94bb097dcbf4eb93cead9cdaf2a38cc3850528ad4c91e17167d8a66649ea04a96236aee42e35ecae20cf13b95cf42f50bee7d0a04a8c032a66249c90863b43304bd22956398aee9188c20d3b36c3e10ad48b8247ec71b15b38c09a1bf29325385cd2660ce93de774e0d313454fb46d3252404e5ea74eb73823cb8fd43faee14250e3c1d9898d408730f0f71e24dfa05ed077bb6e7b68741cc4512c07460f452644ce1819217e328c55d124e3c18428d171657ad260509a6605f7626fe24ad0049f29ec308bb36339de1e71dde3134e1e53726d1eb5809ec18bce7d52ecfe9b0fefb303f8539d8626035111f695ac0eb52f527d3aacb637a86724bf5d5fd27bbed71a07b3745b8c09a89eeca70144008acc13d8661a67ddfa4f666f469f3b4b67232692c228be3a8b7758aa6f506f98b85cb73b90629485aa95402c5cdce3669e274988afeca7f690ea944aa6c469a42aef06ad267e38bee5e33525d3fdbc7ce20de61903d7ab4b2add37e513a1fef8886c52a4a4b613b5da06eb39b6853d7a660c7caa45f1d0b79114cb4a0d4b69cea4fea529dd81bf9bddc7c37613d577c53ca1a34d2d6cc2e3b23ad0d9ae3c72234d1ebe4e4cfdeea0f44ec23ffebf2622bbcd75223a05c11edd2b02dcac8fb50b7a9509c95420688a8d88ad058c96041326e1baa7ac2547e357b9e38ba34bd4e22487e9cd98026d18d307032a34b9817c8cd8e30cdc1130b72f347e30ce875a6c7afad4ec16839d67bea3de1606d1241e1e5d1fafabb077a9af7b0717e8dfdff5d1762d2e663abc9747f4c4137d693853e9a399513810964b6f4feda1d900d7a0cc61b1358f2904bf2398603ec267619c69db80a1b7d1145f718b34c70b1da8f83c72eb0c9961bd367ae74322c0ef0b79fb00fb868f374dad4550db7ac906e1299b9b1b6ccfef374e680b04b420ddb2eeb6948cfcdd5f077dc118e9d79c0177e68cf49135de6e2f06fd16bc21c9d16089c3dbc970e3d40e4841d49a8fdcd569e8c5461c6ed70b6251f3ea7c6e41a2281aa58aeaf46298c7350b9393e34c5babf3d9b0d4eacddc0335b54ab475eaecf376bcd1cc1470875a0b9972e586e3257caa45750536112625b18b4976ef4e0390c50b069d1d57036c6f2d1d025072b9ea1d87d2345ce39a574d67cbd4478e22521ff2d4a8077b3a40431ef2f66a2859ab284e0eec76631fda98f733cd3602a5ff1cc7f47824453a04bade033ee3e2eeb67f81f83d524ac0a9f6585556b3253e57f744ed0c80d09e9d78b09e6d0b7bcd9c5658bd750c0d9a6e53599e7876808411b73d9c52761a012ea5f5b6b484b99b5d5554ea56a4f9c93eec162badb8e51fd74edf4f6671c52dfb9e515751aa677fbc25c7362dbd6b449d1d65cdf5aaba1c16efe09e31fd71d7ce4622f9f0ecc0db7e0af299ef94380b4703568e699f696100ca0cdfcc1c2bfcc24d0275a956f201f73aa5dbcdd532de62411164e1ed9c49301767e2213fb846a0ca283b098492873102a5f2d852c7d971ac607835d0707cd453fc440d64841bd832221c66a2313b9c28412f138ba427f864cd8f27ddf6eb30d28dbe19a5d6ad6cc4b029f5293382f28bedba4bbec215e40585e78b3eda324b614b6cb0cd0065256b1df67aa63739285c90cc2217a13cc29a65bd097f64f7f2b2a828081f1c5d3a8d96651d418b933035c4729f9601b9aab1e961821e539ddec1a8c5b412c4d7e09906cb18d6655b32c22b0b857aff0aa245bf1f0c91a6fc3ecba6736a3a2a635575530461c7ab7c0191917d5ce3f2d6a1ddf52af3487d59a48e44f19850cfa86c3399e35b2d3acd624f11a1d1acb4953019579e463515ceff867974432dac37d3cb43e48c6ec4d738a34f8976ba91e569f411d8f3906964845aa4dfd21aaeaf1ffed7c8342536fa761e7c7cf0101063e4d716ee7f417fe24fb14ba9710df1009fe3709e0aabfc05fd893d7e10dc8caad0536fff3f2e5da2fef8646a8dc2bee9844e66df6a25c3cf01e74f657d5ec44065b9921799f1f6837e1875fe4696e680f3476afd20eef70c19e85fff5e4c6544bb1290ea7848f9a94e2b74c8ab24df83a82897ca44ced481a1f968e56023d3c146f46eafb15115635a99c516e88aa770b35b4edc00a086a46e4d478241f202c36acb5936677ebd04e43d05efdebc0e020e1ed3ee9fb1facc1eedb11a5cd3ed2fe1681728ed70ba5c40bd84930b74a21db04aa9a5f9f6b4017271399e8c512e4032e804af6977b25f9d565a600e26e5693b8cfc4242e6e35daaf588a9b30c0e7bcb6057d6f0302816ebf298af9b71fa1a295f05983f2f8d1fa0e4ac9605af5a04bc4407d4172ca748cd8a2c832a2675c20c6653c43526828f77f6f4bd0792f75da5fcc871fc7d51baaf06b8ec069f1fb789d93df30489329c62a5fcd452104959aeb787795afb43522cd5b547257185b0acc4ec5869cf0a28a49238399a5bd2344e96c79e349da6154a1c19014a667368839056c42e046bdc4a5537d2d2e85fda8035e288e886a337b857d4bcddf52ff0beadf73e5058459d87f018d590e90420d4cdae4ca8c4735fa7a57da7696b9a11e76f6279f0b879a96f35683e05ce36ef93dfee68ad76a7e720d6e1fd07977de6634bbd0f03fd672ff96fdb4bfe578bcdd2e89f3518a7c91102fd090d3a69cb1f90df85c1f5b67f05d9a2a51890eb74ca21fb013edc1011e3aa50e40e932da33066c0d53ae21236378d8eb201abca3a3b59736d7b708b5a01c6ce0ef52c595e6e0a7dce7224184b22e7e3e91a0dbb165fff3565f7c5c2be57216f91feb7934903a9f2beea479af77584699793dd07e00ba53e6edb1dfb47d39e68e5e4268de38824063b2ba5275eca4b1edf22fbe43cdca538c16f9fd305ea6d42d7f2072d7b18d915581f3d62b603e6295ad39414ced9a5247a0b120f04dad209ddc925796c4098ac9d34855a499266968a68642c39f6600ee7c070e7f06b6d6dcff66b17a75727c3e8160bf7a22ecda78e0a4ed6bf7ea6a8fe4719a0e74f625f71c27ea4ba6fc9b99f4af265b1efdc25385d7dabd04ff69f4fbff6f757b57f1c02e83f3d685be91eeaef158290f6a99c98d21ab38bb2401d5d391e0fcefa97b3f05e13e447897899913b4160e8ce5795edba242ddc03beae8df7db76090acb753d5cd448a5ec5722d44b546bc02439229456f43a41e1795a711ae5d159058a9a42433a7a4f1e405fa776f7a06d9d9e514901bb2a9645642a52c7237794ebcd6cbd2e311462b6557058cab3a40c786db37027c9a150b1a1b75b160be6880c34594eb780361a9daa91c64e0ed1f498c322355dc46dba5866b6f267a373123e1f343be8e31798675c5c2efa5730df2361cd50313a1a6a664e0d499e733d54c28e56aa1e8adec15b09ebcac369d695f170b9d11c5919d5d401630ad5314867176d873dbd2a537766cf7177239e068ca083a494743397bf08e7b817599fd2b21ae33d32db5095d0ea6b5110288da6c16e159a9f605f10fef0a45d9de6cd329d1d605f254f01ad309cc9ba40211a29811b9de286df7ec37dc9b2c640fbe78f7f5f00da03e43bd4fc200c0df656a00f4ec87c0cd775ac45c2b13e7a4cc001498b30b4cc367cbf9f14d11792b49341e003ee75566f77ed527689b9256fb5934706fe9106192e1af2d30d4c2ca670b720973455eabee9f81f85359e2c9dd72fae0dd11a5f5f1bdbbffec2cda2f05649f9f6f235273d04afbec2f7a053fcda1ddc33be3fee2e11e6dfb65fcbcd01e91e9773cc9e2252848e4467bf5b0e584f9d812d78c80962dfe86795733af9cd35fde04ed3ba077d99d9fd834be4f5b7938bd9389520003f31d189a840b146697965f8862076921c4ed6bfb08393bda5f00c7ea6083e0268aeedf4780c6a8fb43bf840fffeae7f81d9ce3461089b645c0abeac16d5609632d8202ad92d4b1a24b484304fd900ab8d041070059e8e6b1480a453a5fa7b11436107113362539133f740e8448d89389ee39c42d2e6a19549f6f5baf195076cfd8643d019e299a3ce7fdb1e7e6469690f0e367c82f898120f29c8c4db910c0a5820cfc9b52919659e7310a4a1d05839b8b39da2e8ce52ee55420ef12ae34f0434b19969e6c7f2d808ec8a5b0ec738d1a6d05ae63b9af15cb6e3376c3fef602f9878bb69b3f19cd1c173c971b4c456ea20c13210194e00319b082c1f4e89898fd35b2531089bf0f71b1fe47436a981c29da49cb398e759223233f570d82da26073221965572b15b907804dc7b3a093f5cdc2f72b47cfedfeb50261e4841f87b926791c2ab19e2a56d45ccb0fef22b7dec19ef1fa7ed3bf406b51b42aafb9f0c002d578be13448ea9ca6d09637acf8bd0f2e02f2613ec14ede9e1ea8097764f1e8df1b5eebba5b1e17b5acaa6f9a872226a7fa80144677275e4928813b17e37bcbec7125f7b3abfb405ff1b7ef6fae986e984ef4520eb1b00f8b17d489be37ba0c4fd4bbae63b8847d35054a49af1b66f5fea4862afdff73f520a5fd49b2cde03a6a17f1c94ff038cca9ff9a7799b7ce4aa9f20f8618c0baa1f9e5cb40bb455f13128337a1b37e3698f0ffc726af446cb943b6ad646fa0de612ddb87acea2f45b73c957abeb1f73d3f7e6a6ff757e8d2659f95545d161a7897d803f6f791f72f702b1cd441d8bd3571bd2061dd088f6350973f1ccc156258f579db5c68f9d37fcdddd2b9f603feef3e10f7a5972f0d0478e0a23e5c066bb46779b41191fc6e4085daa80765a68c97eca31c01831256d15aaf25e8378cfdd918c1b99703c3a28db5dc5ed752cd9cfb5e94c98b943655175f4e3bcdab65f942cbab3937fca30ba4342913b7e3753797bf1ffb94ae1ef13ff9f95e006ced5630c4b4fc452c2d25d8a622c43d56a190640c6fd0de21f6ab7839e8f7d9a666459943e42f8fd987c3dd4ede8f9f9455bbc5e7213513b9cef3c4dc87833390ec8a55fe7f9825787744facbd7949f3f08850e589ebdbab45442588182d3144b0866a24fbc2415a62abbd4420bac453d4706583b2272fb52f4ea12da9e2399786bb7f5c4395dbd2435772e53f408ba761ae74787af8131ab8ea46aca1699093d1d85d7b42b010a4aa2892c974af2a9c3171034387726ec6a71963e73489a41322f72c129c42093db036d38da8cd25c1d30d8547c1359a5280bbfa6fa3415bf47736bd7cc2fc23d25b9a5f60c3c9d043b29e09c3d888a249bd0a588adfa77146f4b6a2886a44bc1a1ca9510f318879382b26ca30180dbdf9715e216335d9121eb4836d6e3e089c151ee287dc918487148d6e486f942a9ff78966ae6ff8f699a0f08f5c829fb69d26dbea2fedf467d88f3bfda53ddae7eaa6afa599baf4187dbdcc5c1119912b44dd483b17860c9d33718d1d8a735ddad394d1934382b0054ad29872c96b39b4485863d82365de49437c349e70ebd17c2d7b61122c7db99be7e81ed983cbe9fd07dbf3c73f37174b813baea01b6ca0ff76d51f7cb2f1be4430d4678cc88e4f5bac508b0aaf37014c7345bc4197fc6cfd1bf8fece917656ebd01f7167a4798deecb8bfd98f839575e605e74edf345ff0ae67b261410dcf397b28fef30e6a4e4eaca3f9d36accd18315567848444b3695cc7a3e93484acd50258af8979be5ad5079a5579bf9aad0c9f1338ca71f7ea6e471babd50225dc8e0d664da536fa95a19e6760bc957d849e6b967fd47d6c765f5e51a0f9ce578579bba4ac3f40fe40f6f5be0f7d9fba7e49f320dc2375b2c330d58fab31eb87e0d1428bd93c6142cfe6b7bbd35e9ec386a86dc0c116e042422aa304aeeabdbc586df6b63c668502e683ea204475e1f0273cb7f193dacd9cf761b31b7c32c79546985f0f6eef42fa21fcf233b9fefa6c14fce4921e3c7bc9067f7c54f1bca773ea3fd4b542ee0a40fc0643dc79bad86f30c45de0b4545e0dbfe79ac330cd291fe543687d44b013b911979378a49627e39419c378b84a166ca0470b0fe7251eddd65be548055289ed6490aea92509d9d978a16393856d2d76f9572e941f19e2fe0ddf67fbffab5167fa44f466cb01d4b5f8ffe31840ffe941ff0abacd16807380ed52c401e5d74b608ecc8ec790100b49724fbf5c1cffe3e7e8a973cd1ffbe3fffe1ff05278fe475bc1c532e23b59fe648a69b6c9fdd004f306fbc30ef3f6e48545ee25e726e17a6809cc71c61e336785f854415706b08bf07db81c9a6418a8338a9ae39b781515c55235a800f46d752d4c1729bb2d46bdd9301752d21732d9a2763b34f45db3b768157bffca46f5dc4ea833625ed8a65aa522ec5d4b898be0e0ab8ced8dacd36e28791a2a89fb1d824d416759e0db999880aa56f1a87e026c90f75dcc08e9c43d41b29ac03e7a5012294c05b53a7007a69e0ea6c8b0559ade2bf7c56bb4a05dbc549fc03fa2e7adc6d905faf76862dc242bd9aa3e4119e8e1d02c3a0144b91919c456d6472031d2ab1e70926aae371817bc879951b1538e9936f7ac9cc2dd6569829027983e9e3af313b559039e612adb6e657094fc5648e8b923c91fd75ec7fd2275fa5a14c451f851701a7e5d89f31697fcefc7aa718fd4f9f8e6a9335a913ac7cf56e79722e10c272be22f6a795e844d171de619fe23996f0ffb57f02dca0798eb41be9eee96407df2677421e2f1ae5290dc31d72eb2dec28c3bdc1b1657e02e794209265938cc8c178f6b5b583b0b863d2975b1df4e01972a84d09c4e513aa9d5a0db31e656f7e0a958ea570e42ac0515beaa1cd8a50eca23e847e45feb077e5b1b05eaf331ab9b3a74928e8528cbfe0112a8089be76b7bd926cc2bd68f7d2d4ad3c7b6368f0a52a7c0d23bc0e789ddddf607ed824afd14a6e4b1968fe111bb19bac3c38986a7f1e4141ebd1aca5cf3589612559f8e6cad62be2cceb9494045b4312d10ba8825df336d055aca315d4789430f638b849204efc64e979f6f1ce3d4c8ce68b9d3473fdbb9cf9ffa8e668499d177de8b1234071a5c416b469c3b51d81ccada29c2f211f61b1d3e9e5cbaa5b720052caec9b149f94b87d8fa820a8d25455f4ec432525cde1a097c0858dc7a95eecab94ef98a60a7439952135ad984f44a600d6581680c455ac2d81831cb29c61c3693f1a655784b03e29b8c389d71f438c207a61e9fbf28cbfa5a125ad86420c69b09e2795cb08b754bdd3b143d5d5a363d0707a6338638def7511b00330e0b0d1b7693cd4498736b2402568ca180b935f654c63214d48901a3c77887badbc1ee337fbd68e0d089691fbf6fa2c8cf6ba03d037f23c6dda30b1d5a94302c460b6de10f1636c6dbc6409c2f213651237aa5cfcc2a85f6ecc0c3aaed04d2e538e0b385a7e2e48c556589864c6ee4e103a4373bd6781a9b9363bac5c415c5c88cd4aaef4d16eb4d85fb91ae6c5a5f11515faa84b560c22807236ab603f8d93e3e99005678087958255ed273523f998b5eaaab1e0a45078eb507ea711d8c0adf3df2ac8f61277fb3d116f4363f24e0da5bedc73cc7bb965beec16ef2f3c3ba807cea97a284ba6ff473fb2d7fe653b1814b7ea86e68c6dbe9eadfd043ebc74f955d5ed5863de3edd2b0fca6467dc7de8f5fff56717307fa8da2ef0fda0a183b5b09530addba24b61b3bd9c1a0c5e9327466332e1e59cc2a3052938395713ddea3d930a48134268d3809a68e5902b3b1ba550b8332489a0e83ed1a4e9763c0c2a7da2f56c8f84c5bddc8cf64b9b9d8af6479c827b66345f3ae091c57a9f410a415a985f9c7cb56d6a9a15ce236ee5a015de8f9f0efd5c7bf0f7f6633fa0cbed97ad4d5b6f134c6a57af0c3938b9ad4b24ec2c46331cce6eb6462878b35466b597924d4a96b57308470481c31236dc158bc10af4bb62614d000f7b6856e937a5b4f9664b0f7ab1c5f45e16e74828c13173278bcd77f3db4eb4b9bdf75a55df2e6fedff5f0f56144ffd41ff0e24dd18d3835b48f66d3cd4d021b56f543a6deefd4753f005f56f4c76d5b5d973db1824d4fa4b8d20c41701c1c9817d864694ee0598f4dd7737f0b08406295436c4049852e5494c6acd9a30efb760f14d2fda8165920a23d1fb552ce960ee57a2f391d73765f37f56f1495a913e666bfd18bd1ad0eea07d80b42df6eda55421d71ca5685a5c8df093caf93c262229d8aa9169b4ed562e7ceecd86bceb1eb64653a833ccfe3fcb77f05f23d4bcc453111f6c9b0176b4b639524b28b05f3e99e29c59ca373a888f6479088a7070c842027dc31c764818f489c670834a8d3455d2f2b3d3cb9fad45d3914b61cf1424e2f3a7674cb42e866d6871fedf64a961969de8ffd227b7dce56b5b48ef37eac7abaf9fa0b5dc9ecf7e654d0a7ae6f86a6f5dd4c7d6b4ff664b5b18c3c56ae1de33ee7ef3efcdba33af05884f12a9c1e3b93558691878ae6bf01401a57409636c5de63ddacb5597a89bacf52a78fb5b3cb0e0e96018136abeda0659122fc645a518e216ec7cc98ce80e1a1dc92632d65a4948d7b2aee4d56100056b466a3ac031fd2bca21478c1e15c2698c672a139cbf1e408acbb59d84cc7b2ea7e5ce8fa474234dae89fcb6fd1970d05c73a94dabd82bc3419bd6ca61728df63d07588e4b80939c1d48ad5a4deba4472b2675b6a2a446a9d43de82db7a2e94c82769754a86bb2803a2fdfe5046f99e5faffc3473f4245b62f9688e13238df056e8694e62ad2cdbb9a27966fa7553a70ef5fbdec15e5171bbb908d016e5fbf26818919345e82852b51d30763e56806c6da747471f8fd7ce144862040850871d38b1094a43d14f1dd0c48c194ca420aeac86b4e4e3e10026a783353a9346f27089972dd1913b5affbd8d63136374306adf43be22e5e3fec226df268c498bb1c6692520ecd89a2e21630c2d7876849bb9d6a9295fab64886b4c655f8be2fa85bdb89503f49f90f9db603f0a996f12586f55eb5eafd54e66e01bd01b539e2f2f45d25ae88d277b3bce73bc84d949baf1f9753c66f30ab00d79959bfb49721c3150a1278ea70273d543e1d51209d7db2a85c0623874a2787c4057593d8f0b710bce424a43f0606518edd6e9a7ce874d55093aec804fc02f88797cd4ae8b26ab8e255ddd015b934ef401ba0e4ee37554d329ee656da6783986a9695465467acde16df2c97611474fd0af7db11f9fb56d34118a981d78fe3c03ac149e164769e2e7096ed224349bf8c7e57a7cc23960c1204abe4946739ce336c188b5ccf2805a33089ced80644d14e0f8b428535844792fc7c23ce9986f7777e6fbeb53dad6ab833bdcdca7e7861043d19a723ca1078df487c83fc3fd40fbf9ee5272a08561b1f499316ce20bb80054799678bab95a1f0068203b766cb8c0900fb60eb3ce5449d78fc52ea363bcdc18f0119e96027594ade154e1f40c196c38b298d215bf41989ddc3158cab846c3dd21f45303d2db0433db31f33fbe2b6970fdf68cbc467ec7bba3fcfcc507ca2fa6bd0bbcef51dedb6d3d73c1ce271142f7e4485daf0e0e13156b69d4db0bd234cf3724814c63ea74a287b29e682ba4d8efd7c383a77b0b64b28d4eb3c01aed29404186abde00e78d50b4f7dd50ae168e9f3b61ff2aad2f59a5d96b6fd0d785385eaf86c1a3192b8fae79da0f71864fc3bc37bdfe37f22d65dfb8e037260bdf01fea0ede5b65da50a48d3b7968feee49e3123104442293159b89170105ab492bd195a2e09bebf3791e70ef0755aefb76d5378422e99a3846e8c1362bb99fbc704222b1ca5c31ca9b065b95a2607c1f1b0882464cf1757ec82f3130b99c275ba831841d9b964520a956e1af2b01a2ae3929e0c0bcbebc6b3dfe7413f0a0c277ecb2a7f66aa163ae72557fcb928ca3355ba374efc000ff4ef6eda374d1cae19b1aa17b3ddc09fa9e4f2a010303c52b2012cff3477ee1e514d13453a4ff403fc8d01af372f3c400d134d2863bd2832a5829d48db39aa04d1ce712d166cf4cb09940f347eb211feab952a7d5e4f7fbe579bfa3356f4e6335f476fcbeb31de57f3a7376dfc2f501fc7d2d21c69322e4ea6583a9f2c0183840f888d979d9a4e3d9e951c237bedf430b2bea26679aa686f9b38f2688c338b50cb9d28ecabce6d3780be54ad3e2120cb95dfedee6a1aa581049777ed8870c07b39676ebc60b85a335baf98a9a72adf4cf699f77ad3f89f4b843bf7c7ef3c43dd837e47f7db8336e7a7cb0e066cedb09c38d874aeaa82593a69d43b44ae333d95bd0c6778663ed24e82c19bfb7842afaa68b503ed1cf211a91a8d55078f69b234284a0fbd79cdd73b1e084e1bb3ea18c9f058b6ec2ff48bc8b95762f93738e15b8be3aea2f80736f92fc4ef2b647c5539b03b2e6e4503dfaedba97b05c80382ec0458254a0114e2e036a4ad13058fd2161dc0af633967bda3495475acfb7307f87d4ed7db76055dc5d255970238420b282b0749eac7016b8ce9ad015b6da7e5665178a97e67847973285827a7c267f8ef937c78da26141ceaf353465e17dea20a7c7c92b1942d55913aafadd86ad3b326af7dc35634efab267b5d2c2a6f602f337bbbb9acce16361452aeca1056070abb5c4275dc132c96e78a31ec2e68aeb4099e74c6096cb993d1013a78ebe37aef651b6853581cbd59703623ed6673d988188d592fe3019028db8151b21d6beb5c0d4099ef64f99d027eb7cbc451966b37bfd7b0e165ff1a1513a5777d387ede05bd19d8ef8f8968180be837bd691b2301f577886e6ecb8cf17d8d10f99db81ecd0ac41f87d86cffab8ab01ee517b5e13d06e653f4c2a59abf91bde555429f0aabfceb5f7f14a193748c6bb88669677d2df27f730ff047d0e765f5f0a09d5cdc432025720accba3d8fdc688b8d13c8623c38d14c9b42d7a5d5cf15ebabe8820ed3ba013d4fe8ff67ef3d9a1ce59a70c1fdfd151def564323102058dc89918430f20e81587c11786f844771e7fbed134252951c5514dd333111f75d74174e09278fcb9327f379ae87cd662e7ea5287d3bdbd332af6ecbe378091704ea44830ddc806334ce8ce0ef9a7899115c8a103435e7e8a13391fd5d4a8573c4a6646bbc6148de1415700b3a21b30b8e9e28193d693cb4578729824c060b783db4b388668bd932189573d14ea935abef96b03651f1349f1d40a2dd20a704d27b1f71d5b46e54104fe6f7e718f0de71f179bf223b0624550a3fb13da1d7879348d33e1a1220b9a129fd86f1e7a7d2c87d00847d15147c6c753fa17f02a5e4ddd04e9e631b3c47b5a21bb228faa885cbbedfef0a82fd33b602798c6228feb9712b3ddeb86dab5f0afde04e4cfd1ba8eb45bd0f2f4d13cbfd7d2140be6196423f9d303eaaaf1e45016a99037e110d02d783aad137ca00a782901908fb48f2894db03d19a59061d3f968dce7e76d2184be8a5c6867bfdc02172e710b0dad168e73c68395baa58b64135a7b22384d5065be4b0ed6f1d45f58d0923ece84cda8f4a3eca4261b28ec28fa92d7e0cd6496671c8d9b0bc997b61b5b53d863beb48f9a949071cbdcaa2b4723fa1224f4ccd1f8127fae9892ebbcbfe5061fc141c86fa8ffd4d0efd12f5fd0519e5dc34fb9ad8d1c995599ea5a32fa44b4f6030fa69d81c0f97f007dc9a1ab69c34a5f484bbb13893b8458a16b2beed2c9da1aaa25ffc7168c125cf58bbda26fe8529c00aaa68580764c25f7edfec3e783d552e21a7973d1fe751c7919aa7ffdbaa67d03576ad22492ceaabd61b1f59a00b73cb5ac7a54b6b616e9fd0bae15763b7db30750537307c113408a30f6b0c515a480ae178688ec786408b33ff336bfd6c557636c3b97f3d33baaa4eb872b6f38016aca9d530b461beaac0431d2915ab18cc1cbcaa93b5a98f5b81b6fcb5dd34cbe42486e57ddef5e04026f2f5f70359aa861310993cd9cc338159b2403832a54c1f5ba1431ef0eda4f3ee117d34f3b6bf22ab39a80aaa3a63625c1c2a56adab84864da7233cad1d5c6435222d5e491dff5949184c50a1f2c1459c4d44112043006e129c171a7c2f0e2d132d226e19124d30c8366591ee87cdf9c29c326c96189f6b4127d5a85b689d5bdc83c2b41bb2d301b45eb4ec8896db8da70b3d43407b686ca76715a32b3693c04e72aa977c6e070399874770e11f54302d4f781ac67fdd97c021aa0d3cb88ada07ab6e8b32ea5a2beee839ba42b04ed76faaeec0957ab173e4f9a35e9c9570b11ab4d5ffe4c3bbe58a8c47b0fe35557b9268792e200a19b1a561d6e32d40aeffded1b3e6be9f17a15c4d1c07682c8b9b373601c1b9e3cbd7407d94cc8c349c4f6555e42873ee5c5ca1e84d55184771414d30c4a91258d1ab8fd4e9cda035bf5c299e4c190b051cd011258faacb478bee56248fa0433865e78b274cb572f68c7806a45355bb68f44e2ef529edf933d5ef630be4b8e7e68211f1daf5ad43cddbcd5c54548fc7ec5940791a345802e45b79015f467a47d35bce9afadadf5d6eb17fce93f00c9b3e0fedc52906990cd41d7a7e312cfcdcd883195fede5226923db077d1c180a02971d245c13fb009e2a054c7119ddcf623737a9a590ac8cc38c48620cf8e4f300df6d13fc76babe353bfddbce3637c6fed7dcdbbfeab81db2ad18a04f83afebb4dcef3a7d86a78b89d5401a8dffa7774a81fb01836e00c7d30ca66fa36d8c52bbce32ab306fe9dc48c344905dc40aadfcb835b852e3f483e17ebfebcb2431b8c76fd224c1582d50d8ce73aeb430ee229775ca89b7c4e7616a861e7cc747e1cefa298c0d67ea73f64f2f1284ce061748a10867275841d38622c950bd3a355d919c6bb0908b69ba1ae1f1ea5beff917ff168b2dd1eb91fce9e86105f0b00292e7de52aa016c02131a32035ea63837a2db20cae322f75713e022a39dfb52f52dd4d40ec882f04d78865c519efa10e194816a237a001bbbe09fec2c3fb7366b39bd0cf92c09557f71b46b3738302bd6813a878710c071d96d9ce4a7ab486637cbb62ac1139d6d693ad1642f67176d0c6dcc85d5234bc8ecd0e4271184decb56328fa340f6e537cb6a615e430f52d343a6cdbe517bf09a4fbef4b20dd4790dc7f917ac4b8c44c7da72ed2b75d2edb4564a5e0f341931cb60a4320dfe1a1ed2d948130148f512aa730bc42a8436cc224a2161bcfe787cb193e71709953365ca2a03d18738a1e9ca43b8f220762325bd2e3fdc2c557cbc106b7710263eb2cca4715589e16c5cde29e5ba0cebc883f6be6f9da6517b209d2423682b67b4850979c34c38c021d057414ec7a748a10e194e8ecc41e3e8e56aebae479c446089a8598694c90fbce943e8239567666621fe6210557a627383b1d3ac7f0d4323ee1917df0e6b07ddfcc2c2ffe0acfa70d24e155e6459df105b8e75b1842085853ebcd71a97b7314f476b2672d16c8a64732c6946832e779f5805cdddfbd169dc50bcf05f042e022a041e6d6663471037bea8b3d495593ddd256d963b04957e3e9305b080a7d389e96d88917057caa4fc7a14f1f505b378cc9ba37b6a7698a93883cd944ddb2a31c26947790594fa0dbcd6b410c245e7835d1fffb45dccf4b4c6fdd68d36201fb20faaccb870bd5e8f3edf6bebe9dca0c5ed8ba23c6013b3e8c48c93f4620b2ea36c0144a02a072193d8452fdbd3dc957f197423e5d6cb637d9a3d0d09e7519c7e371a533e8a901eda04b033cf048a3825eb3b342a936f6b8655f7e107d29e0dd85663d7b2af06cd2277d649ab3c818f567883024269e4661ad228e1ea181bb3f5baffd0b0dfc4368e024f89255b21d1be04de8a5417d064d37a19ee213575b0c12119c5af031396a363c12764ba95c3284cc906baab346f07dc897a460cb8762801c4eb8450e7bd049580e3977c930534b25e523bd8f765cb69eafbc213dfc7b6c142f09dc3530064f30226705f8c16752cc238994a46b2fdbbe5f56161049bef177e9441e45df575c75a149260204e82c39d4d05e27733d6d72123b24d7dbf36b6a089fde87a67d5301560cf8a9273f84143da835d4a4074eec0b57688deac2baa88876f0516781959ac2a0294c146b84ab242311c78e26fdd166d00b329832f9f580ccb3ce4e3dcab10e89fe8adb0e92d184d0201141e65eb0a549817610360c0371d0b75570bd7116aaeace26bccb8971bb958c1968d54628f6bbf85dd46bec0be3f1119ef9275abb598fd7c3ca54e87fd7b4669e399e8984d367d8993db73471bc8ce5e9713f18394d66d0f3d2590902c7aaef3548ab46f029b82ad2e769c5e7dca051acd13872402423e728c24fe28915f484858f4de1784477466b9c46f913268be6460c061ad8d915363f0469971af887b1ec1cf6ee7241507307d4c73d13deccc29510206ebb4611c6eec708043f6c8087a95f2a377825e837f2b3ddedbb5fd73b5091b6c17c57e120f07158653537da33cb17bcb72c958331880e8816afc2ada4980b68685248eb3db3c8f280c832ccaf3a4e8bf1f943ecb99d7d9c34b3b3cb12d90e72b143b23d9330d7932d0a3afb7c773c0c9ad8d951f9b7d70e67895531ca662b85733f517a27a99471d0e1f9b054f88381f7577d680c6d217186c9183b297419d5b3983362bdbb85e77b5a224d52106c721eaaa350e7856398281dc41372cf2aa132148e9b41a3edbfd8b5e4daf2136d468db3c473f9cf7f814ac6f7e5479c88c26325d46c535a1ac7d370acd0f37e49b263ddea8278073f911bd15c8822ecd81e3341ccac771ac96bbe0826f409b27294eb4edcaeef4950af383182e22b73296bc27d9f24e5f74e9a6ebb35d683e8b3461e2e546bf36f971fc15ec3d623d8da8519ba07bda3a4724e3f598703e93d5ac663e152dfd75c4032eac3fcbb0fe12a8d8b7627f85cb0bbd30af6fbdb624d30410be8188ec7c1d4e3c5deb674586945e2ebd57b4aecefbc46cdc9f8ef31896a705190362ee09bd8b33a6ec7c045d8b701b1a93454417a0b8df796a48ed90873c00eb306774d02629332d400c5d4943abcad47aaebc6e5f9107b2ed0c7c91b92eb77056277d364335b31bbbdb6ea6b633c37c671c41970af55565918696eaa6a807b1fcffabe72cf1f6ad5c73963bf21ac9d2aacf8a6072bbeb0ec630dd0c736ce3e03d1e301a63116b7249950d87e06c94b56353d0251f7f67ab4a2c62e4ff09d1ead91cb2de395f07878d090fd6e10a6a7c05d299e41ed163d3dd94f36d30d3c21da593f954b153817207a75a89cef5b9e56ddbe614e40bf21bc5ec96ae50dfb62b86ca7e68bd8aba62f27d530f98dae21c0f42366679b0ecdc4ea8ad345f7a82d0be5bc746e101e9f1aee793cae45dfebfd467eff9c42e8432a087c1e0395acef5b0e3e9ac4f274998c657f12efca0c9b8d310f13a7be759c5b14ef7944bc91835076e8b867cc572b5ded51266eb103e4e064e8dc9076f2918990a114d0b3c94c5c9871c443ed5a4eab4898b70d27f5adca8055243ff02d45723f918d7cc9d3624053bccf908abf07e3f583f79eebaaf9d34d81c06c325af802cfa848c0583bd12f4091ee1b09b7db871d63a4ec76d3089f7519dbcd4373b31cf3bd0e63791d36cc469d42998c29cd90426b2e96a2d6854765baf4379eec37b16e6e65f1a444313fcbf1ffb29e6bdf76a7ddda679aea7486ac031342b39dee27b18faca0d3d1675297eece54c1b0b7cb6e0fd5c7a19a84d8a110ccf90e85d65866cc51f7a0d2202af4e6f6b6e3800742c8912d353a10acb3e946edf6ac7ed6aedfe420dc047cfc4c722d29aefbe1977deb49af55e643b3fafeb98dd0f09df5b5fefce41b0ec7f7945f45bf5c92e2089a813b5c980620ec2c4efbc15ada8c717c20cd33161a53a0e0db3d9a87f3d302d3839020493ea506536e4f31fd2c38392718234fabeeb1e467d319bb5ffda43f7d5d517fbf477df1be3bed7ef154d35e05f3db99842dd5830afb061e1c2d9ae5fdc9b1a4065166fa1d34330a94de1573993bd8a3c39453d2e916830f338aea6ec02149bb43a83c09f67a394b56039d890d2b1a874db0de53df0afc4b83f8ab596a77722f9aba9d35cb55131886dccd780c524f11d209e65d966552f76846c7f7a6f9372385144540f50d77d15e77e380a1251f2ab878e31f622fdeb3ad3f4ec60f12ba3fe579fb97d6fe0b453748a5b8577f7d603df204e1dd389af2261d043e8f2b7e9b5e1385e9f268e5efba568f211074179df23e418be9493fadc67faab037fb44af2da4510bbe3d16ba92e503775ce86fe5bd6c1e7f9df45237731effaa03f02cf032d61c9b3930e3a290b0d19edd4c97c174b81f9eac789cd2bcbf5835807238bfe6efc6065412af0568180100767a4e0cc7ee742df1684ab9d094d760d45b4a7903ecb1f37b520dd02d57f3bf42f06db3f1f724fb5aaabb2b4df333f65ed8b5d91076fd1ecaed84342a33fbe4d8b4ba29754286dd9d906e0f9cb5f24e1bc5ed516472380ef2a5bf9b3803df20892d6ffac1e444cabb692e2d23b1b3dd25c5a2dd82ec5a82d84defb93e6adbf6e7a37f0f08e34eeea74acf674d39107d42e1b08ec1eea47ddadfd33624e2ba369d7bfac01e0fd6b49993843eee9db270b391baf9982df0257970f6b4be5508640e2f856e37e1a83d9e33bdd341f62016f70aaa25d6a697465e1a99b704c9f3f7d71bd967d549ee57aee236b1c9f7822f0afd386d02aa7cd668349c682c3a1206ae5742e1be83417d7e484cb01e87098a7462180ad661559e5805229c428eea6eb63d51b6bb14cd2e47834c4257a00ad2c3e5411a8cd650dee94a27ce6966b98556f8b78db6b3c84a11e78366a65a1c114b8ca2a69147b9094b2153959567d9561c2e1b10167c2674ffcdb1a7925915a33a6a06b76b137d61eef4698c0d569d9943a1f8e870524634c435f006a7fe3b13e46fd4c8bdf17177d6ac6e8c9ec4ac7a5d7bb1d1f44e8166396627a45e4e93fdfbfded6fbaac29c50f56ee53d8bb15dfd90d3fc66ab917fef793233fa483c0e771f33448ad200e69125a6530e9e4539631467b773be645c1fc63a3f775f1f14234f3f1c1f1dd48f962bb3d196d8fc6d8d337d654c2b3a4c73baf1141cf9935cdebe3f61210f838aca2815e33936a6a04a7f7fc7822d9b81aa4043d293b43815d2fcc0ec93afff3f5f7ef6be5eb9ab92ae3e671ff077a56eaaf072bf7f9ecfd4ae5a12aeb9bf97305ffb899c7f7ed3cae2633a4494397794673554705a71e1a8c46936867ce4084b0925efd62e53b43ff8b0053a81567517a8d2badfebe49e97e3b51cba7900183a1196cc1350b1e12d9398e0c6c3fb656077b2b2611b39e63e0da9f6cb1e58c08a71c353bca3d893291ad92e51e8276fad6c0b0e6ceccdcca925870472f0aa326290969186a11a048b5dc8fe702fc7cbfff536ca589db49a58e6f59868868b36646fdf1499d442192f9ace59a3d5fb528b101dc5b1a59f59b3648ab14d98bc87341aa03006996043b3d1054e459d1d12520015e1b765476791539740e3b35524553dbe23b2acf1c79c6eb208d4e1713726462e174862e452519f348a73b9424d452a7b0b84ec7b34dbe3ad539f8bfdb2cbd0f6682eba32dafdcc975366c8bce1059c5457145d5cdbf350ae070016adecc623ab9de3d409318cac3c55ac464a6c9a2f10b1661a8dde7bbd5d7bb1742fc6fbf7e49daa1be2a0ccec47a2b6a22d92e0ff644249dde7b53da551bf4b063ffebd73fc7548bcacf48d04b4ee70f4c9a46216abd9681e3ef43d49ea3046ab4e8ba4b6391253893aeed75c696c2bc9f61b6e3156cb7f5301fb95fa78f42ed96681f62afcde49a5cf90a9ff476845005c6997a394e99ab638a4bf14e81c274a86bbd9d662e4701dcd978cba596a171ba18b187a1b2c360cc292cde8016c3bd932b48b4c487b4d43756fd2edfdf0be99189ec760bdeefb3423dcb7b6f05d625c8ffa02dfe9b73fe4756faff0f72cecf8dff2b0ae9b385d762abeb26f5dabd2e54d195ac0659b4044cee7641642013db49668b9986d1ab2d0c6eb2810a6ee8089ba54ee2f1dbf96cbc94b0c8f6dd881216c656de7ace221b053378d785f141578226d26476dacc5d2a60daf5aebbb1ba0eb5e9e397c0078fee175c22e9175cddaddc121715573cdc8d5c9f4a3edeee0577d94b239738f02ec583bb01a1c4e6686c26699f9d4ec0108d830968d0a1ddcfe48c8a06075063ad608d9139dc9bdaa279302c36c099650cf96b388bc0a0d16e6b627d39c9b7287b6255b37c625da7f92614925b7310fb65e7b48577a22e09054924071b61d6b43bd44e823817d339b84557d3d92c109dad295ab44304e8e8943a2c34db336266ab99086f6d48e2c185eac38b21c8b4b3edee89502e43f3fb16f3ccf3fa17b7fc1f44df54f971a1c9063e046cd6d0a2d4c3bce7cffd43966da1e9746270b444ce9bec773ca32bfe4527d783e86be13e2f347175553347073b0ee0f9cacc82e92c2dfcc9221406b469f499d05c2fb7dc119e4de722d9f520746cc58ec039f36caa5208975a5dfea08d385a933a01ace4a098e69a99e871b470daa10936c4e3bfc2641a5a12e4fe2d8ae093e5ed9e7ab8b6c149b53db50d9b5b25f15a0352b5c5f9ad416e999221701b7e8f2dd350ebcff962ea74dc05aa964dfcbf9579e069516de259cbbdc04fb9d7d25ccf9aed0cfae8be20a19125a29bfe6c9e6f375d663808d63e5a34486d4853ab9e08a44d4cf059e0b914a9a502bdefe380cf7da114269eb690f21ee9fbd3c1723a19a3503c0345e9a0f3938d0b8553428d20e2408dd2ee52dec23d683084c0997cdc4d378335eae4f880e846034c8f90c85a72e14eccd66293bc864c722df53c22f9a1078492e2488606b896a2f95f4ca76d4274be7a11087c79bbcaac6b60dd90e1d483390b2c167d90594f47492816c1861f9a2221769d63521a342e8c8cc51a175dd22fd2b5c79b368a6136ae2f9051da03279e6cc664d0d795c1e488c48409f7c7edb246e3502d002588a247f7fafff1f484568491169f15fb61297ec9ae904951fd50de66b5761658693f2a9bacd020800dfd7c46f792f962ebba663cf37b9bedd6b4e1c5aac1e89169befa08d7fbe733ed55e6b91097a3a6c17139684625d8c3707f09438305b176ed58624d99102d3111079b60cfa07a6f1453b294ab33552a47c674b34ac3fec830523b2bb7f3a2e8b204b4589685ba1f2f02338c664dccb5ac5a7ad457639b29f922b352c2f900b888f9d637630e954018ee2248e0119f1c7646166da4f4221cb4a216fa969f5b0922ad5a615e720ffe794982d68ae4cabf7e1500d7f684065c99dd7603d5134fe6c3791565ffcd5004018280397cbf8fa253191475975b2e47814eee09de7f3f1d7d6bceaa5a71c142e8d627ec64b55b99f06fac459e4156edbd65a90654bf6f808e0393ea5224f1b81884db8ec5b979066538071d1708e41cb1698ea0ee1cf3f7e2d42c0d8d1f20c4b1bf1f8d04c7168e1a39148c348aa6bc735813ca441b4ff2c1b12b57591ccfc504b4d8b5fce4951ce23960e3e74efa17e957153c5cab82371a24eb89da49e1fad032f770a4d85398a1a7d170cdd106ceaf3bbc481706395ea710cb50437151f4879468f46650c914d168b858e4724e2563884e337d329d273226a4b902b646319053e39fb7c194d7a2c54a10dee69fa7cc85eb1399155b4910018e56d6f46f2d0eaf08f71538fd139755b5e27fffc357dced5b136fe029bb95ed2bac0fac958bac925cf14bcba951e17c60cd9c629ee6766cbdef47c9d845573eb42e10ec34d80853321ccad9493bc55a3feccf8eecc25303d6c1578715ba2db75231f60e19b613bb93723c1d40663c62558c644d83dd25fcfa8f9d62de27227a1de9477dc3a8576fbf659ce6fd0b40e0e114a8a4365336df1976b719851892578e761ea7745276b73a74099cb084bd3ecdcc3cf12681b2930e8102f61c73cc78632fd30f20ed13cc80d8effba81dadc7a398b14ece2287640ca7067fac6c2d8e3425bd1975c83b3fa4163fe090235fd44bdd900f984102449a1b482a2085750c6470ab75ccabf8eb68f878b142236c60702dc2933f5f071a489ee674df9fd82b6732ddf52975ec1385a0fb7e42f7574b7de71dfdad7492887ee94a431d7253913411d6d87732830b362068a968a8e39bb473f03add269157e74ffe7aa703fd8db5d4cfc756c7e7097011f7bd427698be5c04aa8d63d0bcb73a8a90edca1c656bda49e6fcd5e600cb6aa7a78929991d06a4e12c7a7b8a831954d78ee5766110c230c123452e87b447f13b318996422743dbcd0fffd779b657022f0c7ccd4f80f391e53e6c7ec0cf9c21a6149b409c7affbc85b3f97ef3e46de3bd8073be3c57116c7dd6e1ad3bd5768aa7e7dfef7bb75a2f3d09bf56fcfd250069b6d3d5b542dcd030fa7402874ed762828c238401b796a804a4e12106c93e2fc1c948391e55436762c8a5884208224a1ea328ed95041c38d4ae83cbba576e536e9942bdac688903f3d3aaacc59a3b2b23d1bcd03d2fe56fade86f9ba5af6fb856c3cbf5a6a6eb52720a922e236fbede5b23db919c352593a24b7717966c2d4fe14c4a977a39e2f69bb542942b6f770cad13e153ddbd36e8a89423688696e4dd315a60e89032cd99b0ecb675457ef20dbea98c1b6cf7d7edff43115a0c7721f4bb9a807ee32dfb42dd8b9e2be4e93650bdb1c1968a6bed2162cfcc26478a3bc82750d0784143474bcc484fb9da2da3e8780cbbb267bbf3554f14b9d9a1afa7f86637423a3dd763f2001b6ff724439171660f21d4543628dbc42d964b896286522d5800d40a0ee2432a087c1e5759f24ddce598ca68bd7139b3e71ee9721028d3eb709cc3a8b0b59d09375ba583fd74242f369abede1f11bc14a65c981b680fa7fa0371a7134e19a1fd93dde9846bca4d380965fa65bb36aa988163a9d22738fc4313352249d1f4d405f4f863ac7e78e0194c19ad6bcab99cd6f388f65b2cefce02cfba97531d805e2cccf743038eac977ed4b59458a6e8d9b6bbecc92c4ac57971da4438683039ef90ee9a8a1d73bb8acab59045bb796117f40ae958fc2190d25930cc743274a2a5948a9dae2586643bb57b966f79926bc589a5001767cb779ba2b9f215557d3b5ffd55e6598f97a3663e7a7c462a9d0d8f0b20b5b5b2489e0d1021d9cda459a7152c9caae952ea269f3352cd9ecb9521a03efa0d6fe123ba0a3dabe04a407011d4c0082e6c1283f8780a4abd09abe6bc9db0a1e579d918e3a28dd52b691401fb08e31051beb0516ae50c107232d2a113322c497ee664a2b913c79d556f046a26c691a84b2a2d6dbe5c93cfadc893ddd28e41e942ff79ee16d083a1f7f49ca9b9a116015ea0a6ee79a6f513ad68f6cb5c8a3d4053ad1f3cfd4926fafabca404d12541f4d17cbfdd02d4d2973c4b012cef237ee0c50d5f17275217ff71efcd34a3c0d380e43cdc015a763f653f7a457c53f2154d05ae8debed7af0d50df342c35a11035dc23d642d49b4e83986e59dd5f6047bdffd998dee695e1095d781fcbfdd2778b67f3c4b89822a57fd43430f6c864f94835f22ea3fdf0e540d702df903a7fb7d84c84b9c55f721a8e681bde84df9eab864de9188dc5904170a91c7db6f89487ab5e3f2f57139f5555703245f72cbd317f4426d1cd935efb81bb49eee34dd67330c92339269ba762c24f314a237b63194760f8536e2366077c7487d63d0b18e037a0453d4a05876a2ce664ec4034ec2195ecd1163d203172223806b4ed91b1b5b6563b25d14d1fd18f0e886fa47d6ed8f9efd48f979c7ff873cb381de61b8fc07fe0de14f9dd4be2d869f7868ae1b79b7ae8d3dd1f45bae165ba79b67f4e9b7c6c90a818fdbe877be51a886d5f33fafac9ea1e67f20023fddcbe3ab727e4cb8f9a9a1faa47788681994f821bee2ddbc1e031781cdbc8158920d96cb0e892dc78b6c33305118214dc256f4e9ee40ae07c7eca89de4118fc3858d0add701b296191649b2933a3bbe5f0806dc9fe6a4b4613c9ee7407d65299ee5548f8221ef1e3ea7b17ddadab29a654cb5285fc86e03676d8bde8bb5e5d9d0317a1df77e6e1489f06f0c1213578581e9d31af419bbc737202641697db3e3adb473031c20a362a060631612648e8e28ec48a3d02db72db4588c9080ea1ea465c6d77832d3e73b9cda69d1b42d5b4f0165a726369ba9fff3e88a49ed81ea17a3a969b4e542d033c4b555d2d97a2fac086362138ef5f71571f8f372a64ab06cbbd35c1964a5720d415210fe243ec0cf613d0f458515ce73ad8db9648311a88c60c5538c7163d4e9c25888b32a1bcdd812bd5b0fd5e8a85e37e8a2e555a85a4d0386d52bbe5bae33b2be0230afa29c1f0021a7d47080f3dc749df74e406dfe668dfab33d6a2ec2b32bedf50fbe9f253fe531d5e2e5ea2411bcc921e2d90c609ddbabb42a17a2617ea23dc8cd610549a832449b935af43bc2777b053d02bb048eecb84c7eae93a1bac66aa4e6e46f486ce2467c8332ab394ba2c1182e37dbb8e25f9b10598c9851abafb00d97c9e2803df0ed2e863dbeec9c87d5aea3f6de87bb70096f77d56097c5f5312c0b4e2e4dc86a4d00274c975e54f43eac928fd62a752d5dcf746fccbecfbe8324b3c17d0fcc4ba0bec7be2c63e370b208c82a27c1c252a4aeb271be0b29a00dc40b912d4be68e5dc1a225f72012bfcf8dec7c4dbf09f4f98febbeb8ee5ba77c6ea8b2160b85aa6ddd09991e7c9fe66073d69edfcb5baf561d49cc5125f9ad06fb8fc5c3db60cffd285fe03fd269ec174bcec0350147be101cc2e1ecd1bae7dff09f73e509c8b555525f712aff700c5b5ae8bab97d612876af97eb5749e22c20fb6e637f15717b6c918a8e8bddfabee923c5525f03eaf06eae695a749a47688bb7ba09422e3120e03ff94e8a2ead8cfcc0dbf5ecdb1b684c41fe241e0eea4391971de9f638986c0e4413650cbce3741e4f74b9ddf2fead3b03f0daa37a3c2bf7bfcb7c2fd9d3d7e5553244f732f8b8faf40acda99f39fe22b0ddf4ede80e5d4aa9975c2108d957d6fbe3d14871e517a26828ab3b1a6c43b3503051b1e496b9dc537b3799a5b7d21b3058201753392ac6dca7546688e2e76648a885c1f2e1696e8a7d170f5e7f94545d513c27fae04fc4d30affe4de8fa1385ff7f9bd0553791d48f3f6d71de3ec59febece3a4f2c5341a6325dbd4539623dc2da4e35477691d3bdc04e9d12be58fa12e1ea697772815ef15f63cb3d6290d7d32367fa0b487579c15f77001a824376bee6b7b6fe7aac02648179fb3d6e0381290809bc606078f8f03fa14c3abd0f110697d906daf238ef9b1c2235b9d5aec22249d2f444fea6a25a2a3ae318b9c615712baebf903f3613bd59b520ce8ae64fcf312115eaff89b21513bb43c6fd5355778251a042e7f818ba066fad5fa5b675c600b6ccb8aa8823230bcd989928641c7c4efad0edc189f1ce421e54e89f8e0993d72bec6c6686924120dc243be3b112d84a78ddd0666f7e8c83cbac788a4f77f3e4d2aae955a6f2dc75f2ff3e2abcdf6ebd78d283b0ddf9a97d5038696008ae4ba5a54c1a87d0c4d2ff07c417c594e68f5b2ae2501542bd294f352ea6e96af79d4932cff1e2daee6e5b19600b21b28ce8d3fe96d612f1e17e0b69b58f3546e5a8a79dd1aaa7faa84f01bb027fc1bfaf5bffed7aff79570696cf7ee8386f0924f3faced0fdd97b9e447fde13344f8fe14805e511d7ed5f58efd882d37503e09c643913b3ae86ad00996430a2b433937516931ecb168d7488bc10c3ca686a1ea31b24b16cc3ada4a276ad0d30f6227191ff5b87b626937eb2a6064d8f25fe81de7c67f4540790ab6fad5202ef1b6e43183e47baf1bfc1b6e43baf4fe1d772e9bc71bc0e535dfbb6dc295eb296ab081291fe3cc833d3043c800d949091d14c2243b3b6c4978f646e1f77048c9439ba5828960aebaa380ec16aba5c94f525e4845811e1a1251caf2523d0975c8be7fe4b669e0d678c2c3f84ff7f981b78be47fd79f4f0af8d7086c68047e39185cfc1f7f0f7ef54eee5daf7703a309146b155c26e0349c6d401ce50e344a508613cc252f6738d6d96edd7d4f1ece37b15cec853de7c036b670b5d1d45e83fc98d00e06c11f901c4697448ee7d315129d1622e5710af7073dbd325de3f7deaa6b8eeaf94eaf2ef6f2a682afb27391df7fb2d374cbcf7d38afb09f1a6c342121ceafcb2d81cbdc441965d45c5669b114bd789af236bff7f78c3fd8f628cae832a4b4d9472bd9b1b202de1c4691867396e691587f135b1435d37d6462ce06968fefdb45be3c6fa9a25f2bf4734fff7d10551bcedd27d9774abd5ea9b0811ab4e2a5b537b6914ca7fdc5d81f67ebc9c450b7d4ca858ffcd4e18fa8cbcc4dc3da05acb520f21e321efaf6e1b43dc49dbd3c43a7c1c19a2d128a92fa43919d169b31d9637b70d86e9fe8ca70e25a7172a3397913b1d1980625d7e438501c2d01d4c8aadfeee9feeeb7d3ff83f04b053c5c022ac9df85b591d38e3a13f6aa83f58dadba25ba8b5567360f8e9365dc2a14bbda89b8189217b7f8fff93fbb6f5ccad72fad50c2cf9af87814fa5e9d0f3faa03366b6591bdbce041ad9f972be0b306c6987f64387f2e78c3d3ae9ba3fd6e044fc249a15b5b12624f01db61e8bdaaf624b15b2eac9e6f646bc4ce20356112744f173a0b97365a1e596f652ea748e6b1265ccc65ba49ca48b590f9a2c3b71843cf12cfeaa89648d00b90e97b4c9562a21ef82334dfb16b79d2f3f3ae728c6326d82d3818eb4fdd884c6911e20f6e416e132e9972e6529a782604c1e04ef498a2e0f474725c09dba5d74fc8528abbcb2ddb125325d60aeddb3ddac7f5df5f9cecef047f28f11a82d808791dd670a6b754fbde7a8a04988d6d4f0c8980ba87314183d610446a1e5523d6df4351b9093d17e77ad80c24780299ec9875b8fd712d306c773471d6522f246765fa7ec4792e8aa345802e45dedf0d6dff947b29d0edac69783b96777a0923cff13eef6f797fbc1872d9000e8bc96a66ae39039d4d736ecd1c7b51475899ecbc439bf0062d15395f8dca94961746c75d19aae2af39331d75616732ca74bd5d53d7a2c80f6ebbbfbffb354dfd5c5f4f0b827ab8ddc66abc493d2bf176fc0668f75d0b5fe5c474dc5befd15dc79c287814f7362764b2ea1db85693f9b3c3e7d979f46ef1f82342e0ff6d168f560ce8a9eb5e62e52f0c4481e53fe335ff9d4da09a7755b0cd6fef34dd1a82803e8a144b1a27691f5f886484cc08745d507a3698c77fbac6bc10bc03560cf892ffcf4b4ec3af2f3767ee5ae957fa6ce7f8be7fc1756d7e3b7d033452a3b9f121d379e4a470d2d447f3aec918ba15cfbc6248ccffd853f75991d2fbf8f36f9a60cd0fbef20c7d5d1dffba49dab94922290cbfc269fb7982de45e4752609cb66a073c9764dd8a7fef0680bc5c23950d9a0bb5ea4f3691236009dcb23ab1648afdbcef570965815c14a34a0fbbdab0102047c804c9713e7745c751c6ecfa9996617bc3c1b42ef394ebf99085f22acdf575fdd5a0a6bb796aa564e3180355b2725f2b6b0769de38485e1de608558337d2d3b72e1e42aa5f8dac1f2e770a74030ba80732393a5e57cd9c39493aeea7b6eb8d931d96930df98a44ae0f10055c161cacb6bb5a52b2b2e7d05702dcf4aae54a85fa4a77d4651bc7756b5b1abaf3241e0760420cdfc812ba69c77e27590a6fd220ed7eb7c3c73e93c57bba82770d431a70efa71b5ec2aa37e81e845c9f87eaa25de3ac246f6c49274960c18641e188c4ca8cb6072da800724aae3da79d442a2f975087448ab14bd4ae2590315dd18d2243d2f54e4fd3c425dd95ac986d1efbb64a00f27dee28035e8f7d71dbdbf578967812050fd695a7d11b105c552484121258e507270c378c6c6526faccd04c1e9ee73b738ee146aa31bc4d89cf31da8efc8094177179131f24c5a46b31d6c96e6449ce9245d74fa1d94b3c1267c01a5e4ba565c4768d96b85437495596d2d5647d51cd76024d876440727c7fc180ba6e1be93b1bd23ef48d38ebd95d9298e85a35c83590a2c72365ce3f6382dc29c5894126d7b21d11906930ca5f670681d47a4128c926d1286d6116e9231fd1c7cf01278f073aef8374107df70c45784b7e609dfcda4835f1c4178291f19f2e8e2aa808f2616d10f9822b239e5909ca6b25c3aab64cf13fcb0130605db59cc618e3dc57363311277acde1dacf3d89ea2e57c7014dbf999eb830c9e030c5eb6eebe0a2e681458f04d5041c380821f04137c1f48f07d10c1f701044d8207de040e40f57ed19a608187f6db8ed8eacb2081867c7a23cc1982fc84b4a462b9984dd63e852b4a416cc75ab0dbcd3407dfec6569c9c6d180dbe2fb683816744fdbfa23b1280da5238ce078311fb2f972e9eb4a6195becdc4fb6d4b82f387a080973cb5b72dfa07ee807bf1f511636dd7c51fd241e0f318409b8745ead231178f3e5fc885dfa120b54710a5a149de7a51f6dc5508320483458be87830116abb1f25caa1d35352199a17033c203c5245516f8e0f0c334d41272d0f036cbf8bbf4099fa66b950069163fd5d1feb45e4b9a156074db7516d651a3a8929855881f5e6436457e4a332656967df0d0e2bf0b029d3358772cbeee1048e4171907630b407a641d6094dd55ba2888f0d958cda9465d2dd6c225e5dc05ed86ebcd50a4d91ae96fa73d6460c28d6e7b8f5946b1c79521597fcc18efa9a0351ad36ef9059e01f674e46411c037128e5fe972dbc6d48d29dfc731bff3c03d066f1481080cdba4b30e5572750463a1cb70ea67b2e05037b3f76fe7479ec4629a0488af999dbfe1a88666ab2e41bc035eff3edbcf8ebd7dde6cce536d100c9eeda2eea94fedc5a7e006177965c91336b8a0454729a281a5ea851364cf6acd0df382b2881265d5cedf657e522f86387d04333bb8cd32faa3e1b1071126992f7d638f875ed31778f3ce7c3578ff8a10744a9ffd96dde86fb85806ef992ebbec77ebc54bd65f8920b6885f51ea6e1d76717d402fd270ec287927ee5916a57ff9fe241e0eea4b2d61bb5043b5c1363c85616d07ebd5f969d59125347535df6ccf5cf9cccf71dacae9cc853c259f3727e480781cfe30ae90c6d367dc6bce8cae315bc8c998523447317a50f0e684f9d7e773ef40d8ce390030b1e69f2201b3a769057c838f38eec561646c1c4da4ce30e246f438ad91beb229b407d0a76adbf905510c65aaa06b760855737f1c3f2ee621ec3df37ba9709e5effbf91f5f01024f179a7bf5e709e92899a3e3dd7dce0cf960be191cbafd5138e8d58ff96f0bfdba0c7e2e2dfc0404f527cb61f80564afa67ccc9041d903ce1c58cfd1556edbd9159e274901b3677f6c7bfd8ff3bffffb7ffc3f010000ffffd6d9645718420600") - mewn.AddAsset("./templates","vuebasic/frontend/package.json.md5","1f8b08000000000000ff324a32b74cb3b0304e49b630353332324c4b363632333537354d31333448353100040000ffff1a74bc9d20000000") - mewn.AddAsset("./templates","vuebasic/frontend/package.json.template","1f8b08000000000000ff74535d6fd330147defafb022f1b698b66ba7518d09043c32f68e86e43ab79b87635bd7d7e95095ff8e6cc74936ca63ce393ef7dc8f9c168c5546b450ed58753af1bbfbeff7689f41d29d68a1efab8b2810819e2c0e92cfe98367fe6602beb542e9bebfcd4f1caa4e50b4250c90202f5139f2d58ec5aa1100ec52e12e402db5aa23a024b04c5c64d53e28dd9c536562506965e89c28e10bc6fa14a10107a6012315cc72488b503f47a0fab5e6577c534cbb0003b8e5ebf5dca6fb7ad6e95317e07d2cef747854a6de8b3de86471c9377c597cdfcac097f8ffd10dddfcab4805e6ef574bbee4ab42cf882dbf9e9e65bc942f6d6ef97226e9c090b70125d4ceea3f07a573274bfe815fcd265413b44e0b825adad6290d38cd6c0c7284bd13f277fd64a96e55d368380a2cc35d6ff8e534dc9ced8b3507f5384d16ada5d931459de9463a1eb16dcab525a82fb21702d3c41dfd2cd2dcf62e0e17bc07434ae821e8587f87206ddbc61d3755a21e06430c3aafbc5470023de00f47ca1a3f8f9489d8e4ab35e57863bfce7a927e764439de2b2711c83a84837a497ea7fe8dc71eedd1037aad3c8d9d56b76cf56efc3f8427b6661da04f3107d858620ad8cd47761d733d2cfac5df000000ffff5770748113040000") - mewn.AddAsset("./templates","vuebasic/frontend/public/favicon.ico","1f8b08000000000000ffbc924d68135110c7272410c18f4611c1839a8bb16761d2da8f24838774b7b558f02094bca42aa548b0885f544b133bbd898ae0a120082282282a42d1490f7e5c443c78f72678085614514185909197dd0d8b28291e1cf8b36fe7bddfccff7d004420028984fd26612a06b00500ba0120010049f0f2ad88c17f09160ab493859276fc0ffc1a16bac5428b2c145b6d8d50effd2cf48d853eb1d0de20bf4a7e330b3d6721f5b5c442eb3bf1a1ded32cd4e01a694b423f59a8d4c9833f9f62a13796abdccd69e55e2ea8f19a85b6fd8df7d9280b5db69ee71f91964ff4e9b1993ee5c7ed7d54ffe421e43bc3421f1696496716335a1a4debc4585acf5ecf041edeb1d0eedf6bf8ff6b59e8815d77fe614ea7267bb590c7968e96f7e8fc52dbc30d168a077ca8f7380b7db7fcc90b036a5c54e360d3aa38827afaca60e0e10b0bed0b71565b59e8959d9fbb93d323e33d6a86d0d6786a5c7c61c793a55eadde6f9fe51316da14e2cfb150d39ed3f46cbfc73af8b1388cd9e2308e1807bf1a07f538f7077b68b050d967bb59e825d7e8fdeccd6cfdd08174dd0ce18a71f1d2c4583a5a1a4dc78d8bd76ceef0c19efadced6c9d6bb4e27bd8ce421bec9d2f2c53ead4c58154218f29e360aae8629771100a7984a28b1b8d83bb6cfeccd5c1d65aff9dacebf8a03b842a40b302d0c87afab1c3d3e72e80b756718067514fd588271b91aac7fe0a0000ffff564556487e040000") - mewn.AddAsset("./templates","vuebasic/frontend/public/index.html","1f8b08000000000000ff5452c16edc2014bce72b2648512e659ddc7a004b69ba8756951a358dda9e2a6cdeae5f8381e2675bfbf7d1aebd52f604c330c39b11e6faf3f7c79f7f9eb6e8a40ff595392e082eeeada2a8ea2bc074e4fc7103989ec4a1ed5c1948ac1a65a73faaf754279235fd1f79b2eab77e79d08fa9cf4eb809a4d0a62814c5aa2f5b4b7e4f17cae87ab26a629a732af2eef2cc5e3aeb69e296f4097ce0c8c22ee8a17581ecfde6eeec1438bea250b08adb1415ba423babcc8dc5a787e7eddf971fdf7053efdc746437dca6b34e5802d5fd414f236997b3bebb37d572782ca03a37609ae40fab26a6a12d9c65818019a4a4b8af7fd16d210ca994039a5170e10a9f6888b782399557e492329570c0ccd2a551f0d54deef9e40a8aae09e437780ae4065a315820e9540ec79136a65a5f5d66aa2e87329e27b0b7cae5ac6a53799e56e25a6b342307c18e030d9839043404374a02c77fd40a7968bdc45f529b6af9226f010000ffff19addfb833020000") - mewn.AddAsset("./templates","vuebasic/frontend/src/App.vue","1f8b08000000000000ff4c8fb16ac4301044fbfd8a4175b0fa3bd975f205572ff29e11485ae1d5999070ff1e6c304e3b4f337a1bba9496b9cb444098d386348f8e5b737b0084541670eea37b70ca86ac8b3ad81a47377836936e3e155ec4fc8e8656178798d96c747b801fd5f255cfb54fc9591fbae6d91f1ffa396d13057f5950b0b8a6d6274aa5e9da7155f05cb5c00d3e6a695aa576f3171db697b8fbd9baeca2992f9cea10cddc9d48be8f07b33cf9953b7e09a85ce486e3ea0f02aef9db81f1cf818037bdef14fca9f9170000ffffd1241e9642010000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/css/main.css","1f8b08000000000000ff9c92d16adb4c1085eff51483cc0fb1d15a72fe1252f9a24d4ba0819642fc046b69242d59ed88d9b165a7e4dd8b369269627ae162b060f6ecf9e61c76a6bb0e7e4500153951956e8d3de6103fd29684e204bea1dda398422770c746db04bc765e796453ad2300d5e3f6c9880ad77d4b248d71750eda89d1d6688f6590b5f4acc81fce7435eba32fb4c541257810a5ada95d0e053a411ea60559e21c668841d46aae8d53425d0e37597758472f51d4486b438c064ddd480eab2cfb6f50d31eb9b2d4e7d098b24437ccb6ba78aa9976ae5493f7eaffe1f7eed09b67cce13aeb0ee12f8096966a0aa4de94d2e4b0bac1369ca40be0d09a62ac77563328b05a8c83451a7d1eeb2df0ef65afa7032f478b3938e256dbd3b41f937dc8b261e6b9c861c7f62a5e2ed341e0d357fcf851fbd5ad0afc699f2592c4f335a40b78b8ff085fa9edb4c00f2ad10f2b8e96960a6dafa69de6c9db817a7cf58ae7490400972ff06966b03287781eae0fd1b8d5721563bbc5b2c45251874e8e1d4e84b0ed8d7ab8bf85457a21afa7aaba3e438dd393fd66d721871ed8c117a6de238f8d5c1e71308fe76f587fa0ce2117da8b5467798477f8aeb18dae349b04ee5cc964ca04cccfcdbf26f2fb7a36bd8613d3efebf1297dc75a17c709f012fd0e0000ffff584b73dc51040000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/LICENSE.txt","1f8b08000000000000ffdc5a5f8fdc46727f37e0ef501920c82e408d74ce5d92b39fd6dad579127956d81d45310c3ff490c56147cd6ebabbb9b3cca70faafa0f9b33a3b582bcdd3d1cbc23b2babafefcea5755fcf61bf8a3ffdd0ca2ee10decb1ab5c3175ff84fb44e1a0ddfaddf54f0ef428fc24ef0dd9b377ffef25b9df7c3f7af5f1f8fc7b5e083d6c61e5eab70987bfded37fceaeeeee1e747b8d9dec2dbfbeded66b7b9df3ec2bbfb07f8f87857c1c3dd8787fbdb8f6fe9e78a9fbadd3cee1e363f7ea45fa2883fade1165ba9a59746bb75fc150056f1662b709d500a7a141a7c87e0d1f60e846ea036ba09ef416b2c8c0e2bb03858d38c35fd5c2559f470239db7723fd23f8070d0d0a9d8c07e8247ac83943f81efac190f1dfc154c0bbe930e1a538f3d6a7fae9ab167bad56698ac3c741ecc51a3056301b5977e0231face58f93f7c621274e915df090fd2c1c10aeda53ef043d1160b1df02014dcb1f4333d464db7e42b20889ae52445740342a924c7f80ea392125d38bd36da5ba32a1016d31f8a15afe846f4eba81bb4509bbe373a898a4fc251fa2e080a47aee19db1acc930dac13874b371b3ebb3af5651cc8a6fe3e04a5e8777cd116d058db4587b5243eaf0df157803b5181dd273494cf837b682855e687140f2229decc6ba8baa5570ec902db09f82fe82852fac73941458c6c29594d7c14bae9303896a65eb2718d0d624fbea2f6ffef19acf3316a3f5b3a4d13b2f74439e709db0e89248790d7bd4d8ca5a0ab5145f685abafe1733aee0ca58fe2fbbba2ebd2f341be649362349b350c6499280cf686be94897016d2f9de3f0e7900b29c1deb910758f66b435ae28dffad3a01b2cb6682d36e15f5b36fc673aa4378d6c652d38cbb2a7a5aed5c806d98f1eb4f1a0642f49016fc099d61f29d21c9f08b569b0cac9c892929cf0449520a19587d1f203d04a850b4cb9dfff37d6fe5c7da1a7f09b45372a4e97d69a1e7aac3ba1652d72be782bb4a347450a2efe45c53f5b10106cc4f2aae525939093bbd6a61f24259861f5e25d0fa8d10a7a6471eb05a8d5463f057077242824738f8d14e0a76179f74fc67e3e038aa3b19f596b86278abb3925a44e579913221830dead170d8278125289bd4a9850c05545384bd1588b185662c68a847ada78596386bd602f6ce874c21aefa9fab09992be49c695d080cfa21f14d29b83354f32be498fde0c03ea463ec31e95395e97a6b8452b9f84974f086415b73a8d053ae6b221a20592a86088a4fc5e38f2a2e6e46ce8104a066bfa00617416bb8d52e3d8c9ba2b11021be98d250cb0f824d9a714d3daf89837804aec8d4d7f199bfc5d6657924675101d6acf4e1070ec8ce21c0163e5416aa12ef8fe1ca9337cb50b48a8e0d484d18214d9d1852c3f56148bbd9073c2e2202c870cd9866fd2a345358192fa331b6f2f35078c163d5e27e74bedd1b6a2e602529545345bf64c2db2109ab6f4fe5bc2f9c8042e7afe3421720e97476633c6044cd536ab42d216aee1786e2263c9a24cb010bf66ec172f501519e2a924182d94ca88eec67d2f7dc493444f38ce58795630e6059fc4107f463eb2b7b91ebe584a4a424370cde753e8efb113aa05d3be4072be8e11c02adf6a9584054e90e1dab4800a6b6f8d967545aed80bc50175b4f4a2668a32eae802a08458581e676391adbc9b13879de0aa17cbd40c67e52946175a412fa4a2b79574de55653dcb8cc94dce63ef16d02e9d1b918a4bcd25343e12a280ea62e03499949596af4a585904436172b25d235d3d3ae6017c64cf181a39e727c6c0a26ae17332c4f2ba29306ba3dd20ebd18c4e4dd00bfb99c0d0ce242a733374f2a0b92648cd9e62eb5e0c4942afd5d6781050e6ed7a7529a14f1879be7a4ac73f6646a5190933fb9373a1130ef6881a2cd6c8f8be9f16071519e9f0f711b55774706dec604239277e5ce46282a6efd6f037226074f2db6c84c4c1e0710ca53786edc52ea84cba12ac51d41d145602c294fd14081f33875fcc0882c8e0807e142a47e2d158d51c25f1116df42b0e01279ff8cf577527ec817a2e3309e5a757ad45ac405a8b4fa6267c3faff6b183a42353a7861531c78162fa0cfc0a981fc6bd92b59a28680725a66afe65401beab0e35f22f5287bbe456b90119ae9f5d99917aa3d834df2d33f177efa20088bff3e9c7485cf350e9e12cef9949caca20b9dd4350ce1ba85137bf1192be8c413321bcc2a712f6eda9608a101874a55f1ff653f18eb837f3232445a1de923234fbe1c9921b82a9d2b864151b36ab49a82a909cfa272b512b277f1d9f27efb2948294d9cd154638dce092b39595b2bf521354228735d2c91e0ca5d83504663ac96b5e9f752e73680df3b7d21df2974c8b1167b13c9e052bd78c691fc91eae01a362d85c1dc42392f3dc577768d9787a0843808fa6706bed8fb5fcdb56c66e2d638f78aad4637a9cd482c2bfc2d350850e2e846e9e9b60a0fa13a089fd52f38c30954be047a5c2c82ea2ef6ea85a07a76d1946e96bcd233a7f51d06beb60cc94cab522b1bb326b52673bec572989857a81a94afe4c31c33c2255ad7089fa3309b583a6e319b040d7f5ec3039683a6359fde8b6946bb5360aacd2013ff5942d40b6c903d43fc121b39f6550828623dd27766aed8cbbe3bd4f82fa05b35f74f6c9522c87ac4e0eed628658e81002438fb7eaec357e23a5c77741e0ea433a9187a148bb51c241290953c796e2ce97f67b7155c394e9b8f1f4299cdc7ee8b63c31c68a6ded47f49852ecc882c859335bdd41433a1f374a506047c39c24928b5ff07b6090641a787d7c5e116bd90ba4a44bb98037047a1a7b31b9667e733e7e0a828e7e6ea59c568af082e1b24865595948323d6cf09182f18461917343ac3da25cb0bb09a84b07a8d61023ca0a59b9251430e5a5f943588a4fff4b2a7a66bae09cd722cc4b691bcbedadeef366fef56e0f1d9b3dd2913e331c4d2cba3ca7c2b80e142ea9cd997dd56ca4aadab008ba2e116758e40bc685cc22a21352ebc10d18ef122dc856f517d8d754b39970d7dd1ba1c76c28342e1a8115bac04e23b73020f8adae8ef93a22269391b7cb6d232bedc8b5afc5002fd22dc1699be9c68816c67f4a19a7a982be4f901c656174c2d122f2ce666b1a3b860a9f6346d98653ca10d2ef39db4cd2bbae7943da48deda9e326f681c2ae61d785068e40ed82ad0bb733c308bd789e1c0a55f4bec4634e148a99c628362d7600b9a488a6a1ffb6d42895a1598a49da472b7d4d5254c1054e36cb10e2564c683a167533f689e42e2227214d681e9353cf608ead9c6621425d4e2c9e7dc11e0357b0e3592006e37c794772d150732fc22c97970281259c4cd24a8f90947899526d63a191447217acf802e92fc68517d654414eb19e32ed057daa22875aee35a72f7430e5c02fe7150ba4b3cb09e1acc2d98a6c51a6334faf4d1fb83705d472c2931b9c93eee1c42f7fe12e29ee1c42af3b7346b7868f5aa173ec3b7c1e94ac2535d02cb358c7cc9392e9947416d3b1622cf6c55158d11ed099a753a1400cf7e574fbffd4d54546c68a1691136404aadbe4dd6710b0359edecaeb22ae3c7b13fa394ae303b786545f5839370e681d3618564f9413a567e251818084d1abc7b9973a580c4930c56ce1660e9fb12ea19ff1381bc5e241d8b0ca3a6d59f2cae15fd6b04b2cc5115a16dcbb310ca83ed0f4620745e68f8bbcc071f2c644f4e80adee3a89db44fb246887f1a0b319ec3c3298093d25531c68a6daec5df4719f75554f09dd15cf2d9b5a3f3a61776627da486065d6de53e3a646e56e4419ecf7e536e25f7c53a71a1362473fdeb1a6ea5e3b60b2d3df7495832ce9433226bbb9f4207ccdd3bb5670530b03bb9ed99276bd5ecb888066ed6f68ad4455177673d6ef9b8f46ee9e56b30bc6b5cdd3cc2e671053fde3c6e1eb3893f6d763fdd7fdcc1a79b87879bed6e73f708f70fe53702f7efe066fb0bfcc7667b5b01cab0837e1e2cdd73be8c64ac698a11ec9c4f3c831509bb26380673712b652f20af6961b7d9bdbfab607bbf7db5d9be7bd86cff76f7f3dd7657c1cf770f6f7fbad9ee6e7edcbcdfec7ee1587ab7d96def1ec3d70c3749c8879b87dde6edc7f7370ff0e1e3c387fbc7bb508dc39e52a1a23ecf0d463bc9db0dde0285a6f2246ec430583358497c9e2fddc2c873588ec419888b596c98623a37f6dce06418978e21df995ae64e3ba07d5cf3f2a8b7dcf39e77c3290aff6d0defb361e9b5f752eca5e225fe864a33e013c531a912a468038ae7a8be4363a7726c937667de585f8e1e341e943ca0aef1baca4bf76a31299ee7487f18fb57814b386850c93d333f56ef608d73f382241dea41d4def196fe72ae044c5dd41563619f3da7241f1d070bec62d18bc3725340afa7af13e6ef14dc80b52ce67652d7b221161c56164473c2c4580a95a426e0ae3b4166420bc286cd3d55f9b996bb51f9d346994d3a66d419c32f5247971660bb983b5cbdb8984f7ad1cd9509a17b30a6394ab598487e06e7cd30880356cc1a46d2bd15528d36142aa1da51cf04880be4a56f536ad3f714c7a54dc2d1e8ae2b0e4822f4a7b3bd24240fec45f324793ddbc6ef499c93d110e9538b283f65c35fd7705353b12053243ca6c36fe6425e24c8a78eb8fe327bcf96942faef81261ad3b63c27c9547a8cb953fcf7341408b8c301508d651e81ac3458630608d8838710462afa59fb333ef8e55521fcc5ec5b116739bd704444493c35e473aae5fb12d936eb95dc235fc648ed43f852e341b8dad5a489eafc8dfd968552e5e32458f1b189e10c79f095d676c658d990dcd0b9b02e8e7b153110f71e24cad966c036a53fe87f467fbb4b37d1a6c5137e195cea8e6c2705ed89ea12951f16cc922bb476be70d5d1c4c0be7d0522ec5f96c753e95de4f918d14779ac80ab36133f93f166159f0cbac4d8ae5bbed2d55dd4b5fecc5276e3e7cb8dbde6efeeb7bf2254f1c86414df1538af25b43fa3756e7586caf0060f795af54f1ab8ee54822f3702315da41118e8756b09aa701ad44d538405d2be34239d85b517f46ef60f5eb6faba2b751a24ec5704a81c5681b5bc6a2175fc3d5add1ff94bf5c28933689ff876be08e9fdb5cd7995135d415644d624751d4f572374cb9e326edc5735ec3f26020a8b0864f0842390316c3d371089bf19d1f0e31e41cf3dbd0b031271d52b14e9bdd3dce9fd1f07e36ebe2e8cdd560250fc7099c575447967bd7f8450e298ac2c9f9b38068beb4f7cd939e7958226cddc9a71941e73de6afd3344dbfc1afacbb694fd7bcbfc51762bc3445bbb58ca4aafc9a15aee881fca5e8f50f2c233532040fa1bec5397da2fe52c73e96313307d7cc858ad981d9f3004e2ce68029aa859fa3ff8f3e997dbf797bb77dbc7bf5ddfa4d7ce96b78fd97484afc3a8ee51493baf36fb040bac5035fa4edff4fce9ec87ab0de23e2428914f4cc805a598312fa308a03c2c13ca1d5a75f22a6d1cbccf2ddf9d5d6df7ef3bf010000ffff99c55f49282d0000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.eot","1f8b08000000000000ff6ccd55501b0ed8e7fbe0c1dddddddd03c1dd5d82bbcb1f2778d0e250acb815772fee5ebc40712b6e8516e899f7ccececccee7e6f3ef3dc3cbf773000200a0600e001f00044c0fff8bf8203a4c2fd8f6a5acc0000140cf8df7d8103d0fc8ff0ffd97001feaf30003a000f8035c003e00bf000000058001d801dc001e00770055801bc01000013c0006007f006f8009c001e0077000d800fc005e005f0038401e2ffffc503e00508ff3f7e0100b2ba8a5affe72202c06e10c0a70be0a70030ece1cd9d274cda6c19a23af2760b7789a4e115f211b167933c4c7456ed0fb95fc179fd324a036ca51b702890815729f3bd2a6acefe191298f98eda6e8d3c5e8fc9fe79c778dd4628e95b678a2762650222a710a39907fe20515b2068f877a273d4704eb62d801e2f372b53fa5d7033064562c8ed4986afa2623d537cbb76a71c65b4b4ec9b432bbcf89a7e46cc71a17b5d14ba031afc15445ae307acb141cb57e65a1febedb553e51cd5ac75ad8ae51d0a69497d1ff376fa1b28070580de282a8fcdd1b0c9a59e558fc883503e8a07ffb56e60b6811f9c28c9372f31b39b7f0ae51ca77ecb8d834b46275dcf34d9b5b48a46e667bcba33f78fd0f8244bea7ad6a5bccd05f8733d7665d7b30b30faaaea511bd3ee4f8369036f11e2c73f1a701e88e5c403cd468c834c914e71ee29d0e3207d69553fe9afa3545bf1d2919f96680a5f015590b823ebd971938fdbee49ae3ecec9fcc1339c9e2713a01346eab6636bfd1ad042b2182d298b8347c128469ff48c814686c293ac48c0823610501ee04845e187def5b0e0ab6c586223b60077f057f6023135500bc58cca824da283e696208f4a73974a0943c0de639b9faf995eccca48bd4d997b4bca64c9c5ae15474303e7c5a64b8b32f8eb4ffab63fcce3deba19290c688da6e1ba7fb6d542c5a3a6a2a3eeb1cff3409a9cc55c0499768af02d82df80d5ab2f7c8eabc3a5e38e7facf3e9c870849a3ad0257d7098def0c0780c436d804f6eb64e10a5df0aa41c289508a808d5cc1a5cb6e853df8b8404fd6869c88592f3c37fa72214e06e3333782aef3f441a44f7c3c5210204c002604f721e9e2178cd83fa1817b467220e4e630f38773425f426067d9237d03c5501379be7c59102e62bbe1cfcc97c753d5f6ae6269e89dcb15fa14cef5c8de304c726196332a7e33bd2a3b28029bc5c2f161f187b50984eca6911f8c4daaaeea890f38d33a2697c07eeab8d1356fe2c834c3791b6792ddc03470ad433b93ac132dee9c03ea0d0ee9e94ba28589a717c6bea9cd60f4108810c131d3257acfb09915b20e2791ae1cab002ee1a935f343f80dad932fa3b59a56092f64eaead356e029814957090a49161bb0610c908cba12133815f4c81d921d07d8c520f8aecbde409c5b163d4fe4a2af6b0d1254174fe3c5bea8791e9580a8e4fe190489078c53942a1a7de3b19c93fab9434728a84e1f0d479b36ef92cf207a2dd51221db02984393d519319932828119e3d88952e10fb0f1127b62b0a7b41ee7bfac8cc9c5c4f04d6010995bcbebfb0e6ec7752c60816236a36b799c38d8b0b453027a718e1e476db78beb64de898e020ea45a35566272b4a94964546e5d02c52b869b4313c251b8cea6eda63d19728d4560de982a5cb5c2d2ac26292e5929ffe9bc856f0c0b5aad1748ab79502fb266be260f115ef345f93bb8b2e5ddb5756f295c98a61bd5772c6c25cb63e2d66ed49622a22fb68f3497e4fae437312a8dfa491e4224890f8561f4fa030111747db961affaec1374ef8a934a146d3dffaab2c293a47e08ee6175899d14701bc0610c5b0a05a8fc668a99d06824c6f2a61fce33869171f64af7852be2747a3b084377cfa521f736d11b0cc0e99a42895733fd5c927522dbf7ce02b6019396fa2e269c437b909d9e0a7bb1a5218e8e3e1243c9895790d313248320c4617e175e5fd18aca98dc000e9e1f04d37409a923f93cad3421419a83915d66b8236d7aff63aa9e73d65f943a26cf131e6a632e741703d4c2fb6828af4f06a2f68a162c968c5026178c411693e0ebede5f974524cf3df6b4efc748bbe1d1a4a9083c11c946e0bfd7cea5d3f40644b371aafda7a5d48c8f3c83aa82dc01d1e281566bc21dc3e2f23162bdfb07b968b01c22693c4d2544012d7c4667df71cbe180fea8a1185faf65dd7583cf5a0d3780e21364068267e46b194e0963bf8c8300b4c74b10376cb30052c48514d188787374785b37fc18389a37b97d9052ef01622a616a33155810e0b30c16ecc3bd6585205196384a38da13ead935e55111645039b68588aee417c87a3c13088b70323459242672046c679cfd1dfda2b80466172f8423be9fc60d47c92d7d479ff4b42c35cab2b86d45fb37ddf535e3d83fe122c0a95c8d7cb6f678bf273b8d4018311b67a7b8697cafe6ee67f2df18ef8ca86e0dd3f84eb64c31cbb49a0f363818a6c4aa5b19c66747a86ceeee72926fb69655e3cfe2e912b2d392f29a77e82df98631f73546b8da3c28be54a2487ddd98b3c1c22d43a67bbedeb64d7093d3736dfe9b765882d42607ff05d8a2871f485e1e4491597054ccdb3fc0f60f7c609a8169ce9179e76a78b0c9d54772dc78e4e9b0b37060728d0bb10dc89cc2d05c5d24017a44b418c21ad14cd85b82e57d2382387e89c91345692b43e4b47ea8b096fa9ed79c75de4be212d86fd1214adfb789389f741b176aaa12d8fbff812b3a260518a82dfec771a2a1f332c4f8d1e808298df9893aab33ec1b6ba87be2a07b12cdac104de767fd8d9e9752c25551635aa1ddd4a43e70be7f68a0ef94ec958566ad9ae60b27d2a050889c3859adcc4acb040b67af6eddcaaf29a42cf2707fc862b154a244412476ed271bf1f3cfb9aca61b17066c15cf3e9e5351c2848b878629e408e4d1e8af29967f5e30da108e29c4e8c5450c7c9471cfa49741b2208a63d0942e0a4c45b07ccaaacd4d70d1286cada529a0d0485ee295025672df572d4840c601c98f14a92b513454250d00792dd9e0c049f997ff1ae5274ec55af538b071243896b0e0ad79b28dfc879082099f4289e2867691c2d251c383431b9a8f95bb19025e8880ca7b02b58c43be0e30376a9d3c13a2904e4738a86cafdb58b6d587fc35a2a5a42c98cc5afde47cff9453e898f89fdb6e7b5e569673a282d1befe6bd7c8f69306a72949fd089b531a6c3a86d56dc7eb531e187d3297cf8a42c71d7a89000782e293fc428fbfbb18b9c2867b00d4f2dfc6e69d0055dc33319354191d23c4038ab601682282f8f31e11cd298242f7e512b1fd9d99b3f88ee835675516fa6eb77a63cc0662aa6882370ac87e3d61eb68d03fe74b689d4f4edc5d73d7d168d17cdf4738a970fd9ad458b9d571e52767ecc7d19ec9f761d6861ffdd81ad632a7ed10c153d7d0ad6038615cc578e528447a8eca45a60828c4aed23fdc44a7cd7bea2b6ad9d450845da3e7a66b9b3d4236f538c5a653e57794e42c69e114b8dcb38d1e31b30affca12eb83d865df54ca3e88a6b2b3c4d9fdb64e76baa92792cd93086dd310438d49bc05879426117f68a53057312f183586cbd565a50e4d3b8357113bad0145423bfe0a9a88d821b6511498a6d1f3451faed600c74596ccafb44478448ed5111146a12265510a0c2131e251e0cb85a45829676e16c5ba932c5b63242893c0b38c9ae29ad3be0f01f17063e537f641599240bc687960c9a46c9a36b0342d9aae1b5709779c19b70535066f3f1dd8288d733acdc1c63ba94db946a28b75bfbf4f128cf9f3331109c82accd608e704f312f09f91ce501f54a998182e16628043ed396502840e23187ca14328178461652259639afac2b073ed2b953c87ade25171f6f1317a2dd2d0b9fdab8fafc0ffbe29801207c477a7c6dc2e1303845f664837d10b78bac0efe146f4aa76f405df23938c7a75730fd87a85608f958786d143aa75ab3a278d44e3ae0253d2c724b6d778fb19413f3108840b9462e9ae9aea38b5726d1bb924fcb60c108b87577c7e0c0852feca63d5626223fa336774663ec293f8ae34f651e977c5482f729950a38e3cd71b59d75a9c69ad10ebfd2757ba323c30a29235de1c6337304bec73f099d2b76fa1dbacd18962c32ba8c8526dc5a981f3f4ea53f5916dc763bff230b138c0b9c9871cb9d4d3ebf60a0d3c3ffdb0ac890fc813d575d26ee6e69b4e401b78e1fc3be739bf1dc48fc9c9d7dec56fa2abd103fa7978b2dc227ae6c4c3b09f1e19a307d56469cfb54ef3ef19e99966f649c957cecc75340f6429d6a95692214520bc4c98812c1c9a8979da01cfbd0826e2dcf3f76e8a42a0a898524d444dbd4d1f7d314d5ff5d74468c98c428da91ea3aa3f48dee0b39f656d0b2b9c3924a835f81289c71bd263126d314c7133e4558854499f8b992baeaf13fb7d2622f13da60f078f3d386074bb77ff093304f53195fbdeb4154ebb5afc1f66d9eec6c66d806614c1654cde9a6bc8c41c148e604e9bbda35ee2fbb00a7861cf0adadd992810e751ce0a16ae5ff19a380aa5918fc0c129d18f9bfa4b9202c6df1033712c41d3954f80db05a3af943e8d555f8c8590fac8d9db549210f4e98bf7d991501461e8acf423a21ba4dfae3364779863d8d2352bdb8355f2c70cc8a92774c6c7e94c2b75e2adb12c4bf15192beed11311310b03eb250aef3f5c50d6a628509438003f2c2d89d97479119ac91049e0b4a0eda1264c7b44c2ba7f2cca3da5507c0f904efc9651f621733abe450bc7d450aff2644078c75a1a1d00e22ffa891bb17276b38c5591474ca58636c9b7a7c12e630fd8991cde1fb575d8ae3ff5029b4f9652ef9d88ccfeb36d4a31cab136946a309ccc56b8b597ae628124bce6022893ac72a7518d52631f6cd5e8f3cb8e4a986c30d29d68cb1f4d1e1b479802997ee387dbccea4ae9e494183683a90571cf721d8e85eab0fb9aaaa78d961521559264531ea179784993f1203aa90c0d733c39cb3a2b002f6bd1fe3d1ffe24434d0b03ff05485661657d4e62dca1513165068e48724f699c2b67b18d0a92b3aa27c9ae1c5ea76a5d7eef7cf05cf73a664efd5a6c48657836ed87e8a91fe70fc2af085a2bbfdcec2a3a97898a7db3d120ae4fe3c249f91c884dac3f4c4139528bf21e0b357198110448bb5c1dc86e33263796d8e7b45651a9569f903aa11afb7d9e7489ce0475e92d279004f960648189bfc20c856e68bfca70fdbc91451b20e9b1555196c7304fc783679ab45f94c441744d3f44fccfa27924ab47458c40346cf0a6d11cf2aa134234e3ee8d42a80bb2a5d798aff9c4c46e7f6762159ed25d6d68a045b096a300af3b8f95d547028e7a12602fb741156b1da90c8a62ac64a89129b7aeb5d0689e889ad978aa473b9bbd8cf34be56c1471d3e57f86693cbf78f2a7e944f5dc9bb104dbe12cc1645c78413f3f70051ab2dbacdfd7c3a90560c44c795aa2233808467b6e300b11cfdabaef394493b4bfe29ddb968dae9e1d1103d2cc8745741fdc66e556e30bb0b11c210f337bd2883cc613c3c69b51fa47bfb4509468d005b188b5caffbef72c9fcd6ee3977f40b5d934401a35db901fd08e2c4b3d353beec47ddc77594b45eed73881c2015da1f677f27937f4c4b88758182aaf33dd1f7dffefee9125a71fd15ca0c9d6baeef8ec69b2cf798ac6cf21a4a851a2868b9a617a30752310720b8c88b51e12eb31e7d8df6c4a53162ad067309cc20e1f0617940fff4b5e0fe355e78afb28661b3b8e36658f9c18abafbc149656245c508300ff1575501c98890256913a1bf1305d9c50cd2ff9870c9d4029d8adc86e9d99562fb3b665a99a3b16bda23a33fd4847e2de9927be89c33f0d09d6052b63298854dedd347b7dd23d25b8ff091ac138c51613e22afba29227923042b272369ba7549d52c16ffc1d1f6522f7dd16dfcfc7d3607136de60c5f0be62945cd76f51384c68595392f92c9ecc369e7e7ccad68df5e3ff1e2bddc05d76e176e93cf5353e24459acf00b0b71013b5502cf99037b8de6451384ebd9eea34adc547c4976f5ece638c7311e8def77d02499623a1232690d9b1bd86c43cee31c0293f64e887a5b9959774b4d52be6e1c30dadfc8679686a25336a0309626d566c4c372ac1164684897fbde1fd86b7dbb9ab9f1ae64598a7e298131f478dd9b5dce369915857f6106dea0c47119a1174bfb2ae70bd172b6b4effa04477cc6571e425882b6650c253c1a4863f056fe9821538c985b4975060bfaca9c3346fbe2a010c0f1113bdb61b713d5278e112d3222036db0b5c95c375a5c7443669f9a29d175eb8d2b7d59159ddcae8f277f141ad27eaea0d23382e85de331ec7510efaa2c5e3f7e8b68d04e141f13cffabaf83577503f1addb14e7038e90f35b5d4063ee699677c6c77d7e125bb69f2775a44cf7e66e8474739a8063bd33e7d1e5bc39ca65aca34858814b1d3fdfb39ee108a4e1bfd648d79a088dbaccee23693198a1ad7125632035c7d102daa0d372f13ab91197ee0e31924e45a24ab4d4095a3ffd6347d8064959a7d7fece04ba2e66a0945ea109582376d349db09be6e4058f2c8f8bce9f21019175c6160120e78b1cc78c32e9e4e353a29a3d641bccf613b989e25f23a5e4350450fcef33b581bbca36ddb02e8e1f76b157036317f0fd7cf3859f3a03544e739bb4f7cb67fdb322b9e80c75e241be29490f95a8eedd00067c829b618bfe122ba33411bfed61afa71809818110c88bacbcc75b40b5d252955dddb6a69339b9a9203721f9312828928fe45cf0cff37ff39897509155817b0df15e0879b2034d0d6ded65d9110e3b52d66380e52c8314284fc992c86c219bf13b3d1c1ef8c2d0f637e6a53b44e161dee996ee3923b886dd968fb9dd8708cc7a13bd75aa6f78a619d40e0edaa3686ffb29283441eddf038197f24b73fe0f509833c158bcf7fe7857e28ca1421c257b7f5f7ae79a573f75e6257c66f850159099c32dc7953d39ef8f4914d1fa76ca4e16c32a9ac0e7599360b93633970050e72a977665844efd4b0411baa096ff05af747e2ac33b03dddd058b4560c95a539233192bc87c762488b486795320918f57d5c59405d038d8d13a7e2a2956398d02e7d153b4c5f266e18ed56e0cff0cd5a92b2595cf60baf7251ca2f33d5c58267e172ac246fea07ba745c8ab1280877f9d2659568ead538d9fffb879b152fe6cc84973ba061715b774cd51eed5574710eb8aef61bcd9ad9160875c47a4d3067fbafc23c1fba014cc13643fcb51a08c058926f9620c48a73beb1792a68f86cb15b6e7e88b65aec8eb8c606f15e7d99b8d6e7235678e20a27cd9b0c5f48efd018ae0fb52e27eb86eb03394b9098f601a93849a6da700f8adb8e6f3b1838fed50a1088c26d8aae170dc08014e8b84eb67a53d5f2fed7a3a59ec6521890dd6240ff6f3cd210ced7d99f460e0b51d65c275427869415a9c391170985e7f39932cbdb37614a86b9d9c8904ce626196cbb78559191656f6340594fa2e1e84f212d9c0e0c6a3ca6d445938bc3d57346ac121b6b495d1a80092623dc8666e1205cd09f3824c754473e2f4c0d16efae866b219bba1c11cea9bb29fb5d51765016a411b1dca0406ad9c565e6d75dcfd4158e1a75c80d6d396080790eef7c26b956bb4926da5bd39e03e571c815fa36392405240369b89f70709e11949b66c810950bbce5ca97845b8ee769dc9a341ac347fd65bed3cc5d9cac095f25b5a250f33a78654f09c8125bc0c4bd36694e4bbf3b09f9dce58faa1ae25bbcc3dbb7e0d1d5564a8a29f893f2301d526ce04dd0f5fc69c7d38f98898273316ba74fcde3518da7802d8625046fcbcec752ce5e87919a8d7ac5233f052609e621fb996490b84cf1a68f238e69bec72f98934c99ef2604e541677d497b4043e2fc1746b6a5352727a62e83ff63198d9dbd02a979094131ec2ddf1e5662729e29f99a390720ee69cbd1e41abfe1bf8f4261531317af5f0b0e0273c5eac816d4327b9f0afdd3fe2f53f64a9893488acd0801f5eb75308b4c38be0f00feaf588c08b29ffb77a9e5aa3b1509fd714684258e371ecf14ffe94b46fb66bca8a6ce98cabee232cc2aac5426bc4d9370c7a632cb6846fe4f75b50a851354e6d6fecaad9299df804f4fad35bf015e6cddf088e106df240610a82a9ef34e75d1bc1a02d67f4245d9a1957364168c7bb91d8c378f9e8337770b9404a7ee3ea1089df401229aace3de6b7b238096f75ad3c57e7940aab4c01c38a5d8d0d5c980942a0c0332f8cf44355adbc1962b320ff35af16177a44d4e8cc3a14d129ece1b094d323d8a22ecba0331a4993c7712a7075f6dbb657e67aa0db24f0d970b25026ed651f63ad84fc473f1a6b25bb76310be49569cb22e4f54b8315e10f68209ae598e59332d19aaada31d6eab884c004ed9ea24a482f3c61a7dc1b391572e2e9602124a1f851df9d6b05ff33b913f4335ecce617aad29bd5e119041a36b998d9e98dba0b4719397408b3ade96da2caf249bcac09c2631cea3223375831278ebb6ec145787a84808092f63fcad9e60eac6a3e3acb1bc38f2bbf9efc9b9d7aa1aaf2b980e378dfb688897dd6c40ae344066dcbc81760c97739130ead0d90b6ca3e5962e89f78faa82003fd1602825b612d8b20d81cb2c56447d5988be59a2ad43c719e7ef8e7fc9edf0588a78b2e569761ceba453037f2443872c528e6bf8202eb214e4aa50c4087ada2a7acf26f776eacac76b2ac0739fa3c8e121a6ee60fa57f599e73b8343f6913ea67bf91930d1bc6543457783823fe60da032d865a84265b4c201b638ac98742427bd99c681d780342f366988be3dc4a87b39e0a34e4abc57427e5c969d9e528ff5890ca1bc784b2a625932492805cf970f03a569ca78998ff39934fb39fee03c81573d0c2ff561d645be28f112df9a7f9f2fe95d374f996a7def9ab4fe524e7f16580c5957c19099fee5dd1f1c980ab64feb62464a45b8cdfacdedc8a8d77275572b4f7f3ae23fa019c87f8728859e01b63c11fbe644c284c251b6eecb91131c4d341cb7e9737e3675e24ec750c5e5bb11ebc3c13d50ace284acaf682f3eef99acd1152050a9cf487b2a1d15b10325b0c3ca6c9e9fccb9dfd832656106ba3354b2a49f5dca0ba2efc61d9375bb5847054fe90561d596364e1298aa52f702fc87b4d896272b29a93f8e84969e8288d64265bad45708152c33708ed153ae90d548218e365b57bdbe77a997e550bacb17bad51836526d7bc63a8e5203f8b5dfa2647ddf936f4f7c7aad6684342363c9b5c0b059053773ee8880dde5ad41ea682cd7e9e6e0ac90deaa93c460cd8c0afb8bc022cefcc4388739f2d1b5797d1c19b4a7d30d4cfba96418d35f3e0402a558ba5329272737a1a2378c05f43e6ad361e91d8ff66427178d5fa39222671944da27259e4271e01ac63321eb75e733c1dacc06a78032df386c86a37e10553897ece73a12e0e2ae6eeab7561cb04ac14f11f6ead0eb3f98c23725daf584a71433ab9171cd1d87f6b717d7888b34cf0199e4b54f9c7cc68454aaf0f52d5a01c4cd77da0c1f04279169be0841a99b7f9f1e0536d892f9f79fbafd8c80f1894e9f47ed1361bf70990588e8f2f8d92e69958ffe51b307b9a1f1ab24a12ad76a4a27757b70c962cc1f832cea6b94d1aab3204ab2c59cec25131d7558a0513cb1e69e4b4273f0e4b3180448dcf2b5e66a265a94cb314d3169b162d04c62a56fd6fbbeca936cd227db6982694c57f81984e56e5e00705099a4e54c89fb51e70ca2c4b16f65073967d9a9c21068ceb5f52de428570ff2de449a3c2d0d091275ac6b5e47c900d9b05fe9cb0b221329057b2dc8d12da0ff30a6ea2959ac397bf2272ada7bc4bfc7e8279945eb4e85b513297aa6eac3ac4c51b554c6da77a2ffec8dc080666a38e8a402d3fe3453a814e7c83e4fc988965bcc73f3ad0d0360cb17ea8522c556dc4a1bef2c0fb45b884940d1c180b73a164f94532c56e4495f217a5df8c6edb7635745d222e3f1792762a43f157f44be2d73e738ac5363a10b4b444fcca6b3be023483ecd2c92608b8f2fa05c9932d99a3184241ff00cba88c385f96c18301a45c616cfc497e42f01586510e103c7b268854c64f950266a23c7e368ababc20ff793fc9c6ea135f5a4c3d6e72f198c66183d69be81343ba3d49881c36eb6f24b69e7c0329e9cd7ca050fc262943f9e6ebfcdf652b3b59561281d9d6b9269ffb0778de05448fa3e6eca49d6ba6fd5f3aed375ab75a5045569d19f497f71fac99ddfaeef3f239ea1145ab1f3a8960979b327d4dc354d6aab6d44ab4f34d3db21038948d00fd4c067352fc009ac297ba019d752d29747dcda583c780ac3313cb4d0b0c8c8076cca25103506ce036711f233810c1e2335a51db61ee27748dc837dbdc724b0d5be8f1d03e8498a758a959bb5bdbf9c0dc1c4f0c83eef1edbb65a1c50d1c61bc07adc74e70a8357b2eb195463db67de848b3b397ec6ba1f893a7cf4a49c35b164ba471f37cba56ed3bfee41f65453c78a5820fcaff8dbee5c87f3cfe26ff208dea22ef3ff04ba38987aa343045e9058f8062ec78c74f6a9825d428d5e2d6307811475baf03cd5274ac498742f383f6bc11bf12867db77d430702a12163a79ba3e77b4733c498936a255e437109c14d296754244b3c23da56d6cae7d12ea1161eacb4c9d29cacc873b2455bd931448f5b6180b6245a57369cc23bff06433923cd5facda70275f5e82a55b41146be4039ee350560f67c69755e0ac897f761ef5225aeca301c1367b158cdfb51bc35b88645d6aed3a19f07d3ac6b7f8aef09f2baf3a5f4436d5fe8db1a6a4a52e7280e2f5794e697ec9bf19429dd6b081b8b2dc4d472cb129fd2b9f5554fbf3564655cb6a51efcca51ae567242b1b0211c147e6092a7e9a0969f95868684fabae849d1247af9dbaae6e30c892df78e574a465d89e6cb21aaab6c9e343f8da37be0ea55b5f9620925bab71547aa3f1d4119c1bacf19cede1fb494bf2059934f2a9d11925f13e4f909b195629c7db57bb3fc71aad289ebb1246c1a4cc2d29191b6938844adc5f3bf62425ff7d931f8950fe6ae35d377f5d88d640af87e73bf55e6fd2910e5b11c295ea83cdb7a5f6e57b8fbf9fb9d9f4711c72cb6d05ea572166d8b7294d607d5361afd8bafa66ce243040c486552f14ef29f403269dfe4c5d0835c77a8d72004474b48cfc70c7ef01badd0307b6024ade24c795344c23301d1116fd2376243838f66395ccec6fcd19b0707a98c8a97d3c10c50cb200792f9aa99f83a70acbb09937cc072e08b837220c44748e14a5c78fe5c9b94f74955e187035139a193e756bdbcb4f60e6789fccd1182c5a85b63dee9473436a3d3ae995578017d880c1ae8e82035bb444c791cc6ba530b65cb143269574971dd02e637fb74a57ee5f559403267c62589a0616183c539dae46e53f3702f23be0bc29b8906a762881776f303c7537c7c16080ff118de4d3a13ccb3b4307b7e5d18c61dd3ecef0ad0f5f7185c669c48144f8f092281fca18a30702d9d8830f2fdcc9e37432c88014e16258aa39d4b1098b27ae85517103dd2cb005b5d069907331534d8db99feea18bdefa05039b874ee284cc51a04f8508ec7b7e232b98f6e00b0c238906f8ac6bf74dee7ef286aab67312cb38a7ff01e59db629d2ebbfc109e6a4b6445c558c0ee67516588b34eb3d134f0e21b15a3c5c248ee8eaf07b9f655122e7a4e8d9360d10f16063383e47a649d4f42b54c7960a347bf7f065f37e8ff1afb4947bcd05da745a69f648de4c97f8ae75a0247cf21a657872321fd77779ce463967ec76b97c59fbfc40c4faba9c3c8325b9a549ae6b06ff02693a9c8149e851c68de5ab577876411579e7f339f2ce8c164c521db92603caf92f26f66643c934271aecaaad69f88594c0bafbff3dc83bc88b18101d22427259aa4224632224b2b3c4a41f1a361c9d93791b6d0c6399c523c3ad9991743cd9ab4b768468cce7f4ca9786df50f2123cacaa2cb307b521b4fc9712641be5982958c99091c73e4fb6543db0d000b59467ed04dc519dd42fe83bfa8823fa82e4ec0f3f9e6bdd5f51b162afccf9093238bd24b9a92a026cfbdce8c2284ef7e294567845ede8256e01b848b07408dfbdb86d949dee5c13dfcd5b0cb1ce1de6d3a5bd62c29d232399824f3f9e206688814d9ddbff76b3ac8d0db8e123aa08ae742d8aecd86acee2b11f3fef94f5b21e67924f124bce23781d21ee98b7445d428547bbf010efdd2baa89e3410507983fa209d874d1bf0369c136b829449aaa16cc25b229a55798985edcde48d25427ef31b45dd6a58c5c926eabbdf203ec5a7efad2c1c4d47e782097e3f9b085a0f25fe2ad0d82788440fbb222e91352fd4e6fd22ce3cdc4f255483fffa8fc7f5bd5062b97108fac410707c9aca41ea48ec829f492b36ca0d7ab8a7fd1191ffaceac2337d45c2309844c8986e6989b8936ee3ee3fae7a2179c75d5a39250775d5946a961e0d55045e215a384daed2d0f0917cb991956f3e4ef90de2fd1ea932d8c2ba6765b5753d3b9b9ecb992ea9f7a3bc44f95b40c9f723468558326bf1a712726dc23f89442b11d73d65e15d09c798ae1b55ffaa375cf386552ac64b535dd7d330207444393c34a50f7280ba6d1c0d6ab26702d3cae20a17dc1df95ae4acaeef87d1a9b9288f5f91bf4de58dd69ce0ce77a4cfcfcf8d5e4aa844fdbd28164f5ff286cb9a6e33b3bf2f97eef6d63a34e55950e63a743c941c02ef9fb8fe74e5725f629e747fe59f695394759ed80bd4092e326742f58e98243449895cd3b2735a41906a47034ddeffe3e0b5b0c1b24ac43d62abe7a3c4e3e3c13b894986b23f1d1fbd4961ef543666402ef16c38282d36abc048356d5367e9db0ba608dae2e5b1c6af161c9e1f865d7f93ef0f110a60dcef46e2ab042956691e511cac735a45abb2251aeeca486ef25396e6bcc04bdf2665cf3fd267eb46d57f8199081dcf6dc2e11c11d2330b04e68cc6be2a8d770d5e4b7adbfe5c78117268bae74aa941b8ae3f5c74d8c8657dc22f23e1cd7195d83f0eea09fd90642e3905ca1bca2c5e5f094d6bab1354f9a3304c0d9813a8b9e37ed498eb31ac751e21fb296de97f65dae5869b7bc8c5243aa07b3ea7fee9cc5400ec2054a077751dd1a9a0acca5a5d9f43b4f84d054670445f704553627bd3b2480790d1e986bb7c9c858b9a3287c675cba5f2eb503415f4f1189cd391468c8c1ca07dcf6d53f86abaf161940c10dd57d8eff10ffad745ca7ba6b9af044a7ed7f947875455e64660f0c64ffdfed199ce1799b50e0755aa607d60255239b31233b0d661be5e2d799b8146d1394c14816d5ecdd912726933d3ae591743af1ef4345927b66b91bf0dfd7031ae51393431a5183dd016c9f9aa7fa96ef08ab1b56ae2bc6174f841e4118e3494d4c71e5ea08afb35da1ba51941009c58fbc6e5fa325f8779527937d2647f6d718b8084c4d28ece81754d5d03adae670f1ce483fdb9d0118f160e48451ef64a9f3fa9d976c98479efde495331e769e35c29762112adf1993c6a73c3f13d2123f961e8ebbf0bd0e26b65a13dd08bce42f3ff1c5012cb59771ee237c56e31ff982acb9bf34b93f73ccc3a4d90b528c83805ba21f74b00ec7e09bd78b262ec3a67ff56522b553b2ab6b2fa6243b9e94b36b84958e9282088ae234d41954803ceadc6a135c6237adf3ffdcdb1b2da4037c280bb9666938b43604745bd9b23bbd7b424ab58e0cbe6bd85ae3e19f12479adc4d468c145d9050c448c8d08b04f6968a44ec1ecbf3e3ff60cd5a777dcf1b6dd0c2421867f01691ab90f5a0592557748292cbbb3e4f88d1523bb3a54e7e3cb87919bfb067d1e8a9549743bc1fd1dc3fb338b856d80f45efd5d2fb1a7d64797459f0408edd257895517fa8783fa39ed4f6dbe996cbb65236ee7bc65bd7eef9cbc01afe53faa64134f95abcf4744e5c398ecc62f9188b16a7b2732e3a8dfca1ab425ce1a5a4853e649b3f750e737ccdf6a150bdcc15b3a72f837bbcc99a2f4b2ec63f1349dd179702bd3a743310d4a6e6669bf9103b04b18b4f7e88bc80aacb87d12b8500e0ca16775c0f948d4cf166d7d481775f2b4651b2c1113e68ea0a680e0bb6bdfa17c6651e43f08eb771d74242eba9c9ef9033285a8e9ed03dbdfe8ad9988524ace04d16ed1857a3379604ac7c48153b961a50da436550f3f43a3d58d8f12bbc588a9a26720f4d68dc89342c939965e740f231901e04213ee4d0686d840c7ad6f7c594cd6ef59b26918c9ba70ba8c76095652410719316cce6cd614938e3871986969bdd15a329572d87f475f789c97b564fb179c95dd97a06896a8e4806a1054238d4044ef3d5d2502de79ff912e6948149b358e10513b5cb9655e43965b88e65dd8aaf60e5d5d2ef09ea05c201d16001f51cd46081448af4a2ac151b7ac6c239ce9f1c4b5c2e4b674a6550bbddca12d97001b030e5858dc81be4b9869e66505b4de2784ec89efe3a21b713c38b99f915c120f9495d86733c0137250ec662b9967d5b8c97fa3520add5ee8d40e305b59cba14d350f6af8e9f0529c23d7d0920a81247fbeea73267d27671469fd73474149422f7d576c3ae1af193d6c0d7e186478d223e7cdc032c1d5a1a005d9146456d2aab82a6661463dd229b13a1b10cb5d890e52f21c2cc36d18744c9f976f73844e5c7e289eff1a60d2c62283a218ece376e53f8c34c2aea6b9c1d96d6a4bc45bcc1394e37b4aa4602df36773716f713bd0db988b6ae13b3cae223b3c1974c1cec4b234eac5f2bc1b9ed55bd1ca965592a4464e35a1d520876b3a78c1afa903050521262c758574e876829bbfe434890731f6a9597ce442137f34e37e31187c8531f5dfa11570c2c2a79a4dc9032d56728446fe5d1d41f6c2bc931cfca6d37c305ed78c045cae3d9476be6e03fa605c99107e9a14dbfe6a7a5bf275f63e50b48533f2ee37c01c67ed8f51b3dc27b3f396ad7bf2ed2a121e7ec09b7bb1371fb9d6a7562700a592da567920bc0bb21f3a5662592a1c18499e2dc14bd3004d38d65d6ec779dfa0417844fe1135ec3aaed478e90a40e83d674179235c5ee5da88cd3b81d21a0370f3d3454cd88da6ff2ed49d3c5a0917812d877874ef3ff3b5c0f9aa3a912cc2c7c8905376dd0935134ffe7bc465aa5b548e5604548434a738606db98489478170f9d5478fe3c566b203113502a6b56a5fa6f26964d0b515e3da458734fee4d6ae778214d1c3b0a670ec58f8aa272ce8ecbf9c50236a3b9dffd8a6c3227bd2a8d9077f7388e6799526538a6f1c0bd47d9dbdb9ac1c65a5d1607e2e21fa8f7da2895c4b2379cce22917418b5e5811d3be9991ed9e90a167aa117e3599f3b189f5cfab553aa39ae12a363aed9d35d874f4aed24a332ba8572ae57ec33c588187e4e552026be992ebe60236680522b93cbf2fe3f6a2f8283ef87c30224c49e9bc30ddb8526ab564541ff876c89b6caef1ad426646ae56bc05bd8990c97f7445719b25d43c52ce5d87ee3c9d8543ec1b7a253ddeee6a7e19038342560066da18e233f6a4518fda48266011ae8096bd79f8ae81801baf69f11fbab57dfa63401bfb8a0556f67ec368f8911d992d4e51795d4820ad59bff22c90e0207a9da814e2f82d2d82eb534b37fd1b5cc3e7ff7231585e1c84d014480615b211f6425613bfa6eec4a65558c899ead0df1d9729f597fe96eb1dc58ac101795955beaadfb159783306b06cd7255981ef65873db9e916dbf7f9620d99ad7555c3dd661c4fcda803ef93e8de4ee396d430598b6f081ea01855f0c958c6d5179417220edb46e68459b758ff5537c9040eaefaa9dd1407a017e46b86236ececeddf09f72e62dd553197f99044011806e472a73acfa988fbefa4ab2ccf6b6d1efcadea894bfe0dc75479bd093cf256e2e9d2c958fe55e8246456089e3d2c63f1ba5bfcce9f57e7f8ab645f11c6ada7c43bd605482b733342d0188ef96b8e6d7df268f57418bcdacca0b1f7cebf2e50a5bf2aa1042e0356bcdf07f63943b913f4609ad6ff0ad669ddd1fc5ccc496b65d0db6cbcc3c1631440e9d846bd6a7094581bb00f9b59ce7c9efd9e3d109c1e3a213a960e74b897a8974af305515fbc8588ab26e427b11616387d91524f1c48ad6f9ae6f70ceb941cf24b7a2cd8c6e934f6f49f1b9c390ab24e55f11090f143622ff203c4ebecf4c2f4c179883d451d21c922324b0df980b78701ab89c6905edd48083d13a45bf8be2db9a7a86b1ec2a87b6d8c6cc2586a8497feb8abc3b5d96cc2739503e8f3faacc7d16f24f86d8091988f336eba11eed2de368faa5d845f59e807c0d1576fa06ab5b0e784c8f65fb96806e6091339c01855e3fa33a650f4df867a312fb328dbdc2afb5f642758087cfe935d7f34368b27fb463ef7429b28ded2d6d26333f7b5f6d7597b2b508d558fd8ccbe014ea069c8e0651582507893be13bdeda0ba6a984ecafdde50071919a20d2b68e0df2b3cf54df68c4fae2d38907b7dd9f71284432dcec38bd05db00dcf98283dff148ce86bd3724191546760499abecb02409e156c24cd7ef7e3ff6148bc76272b5ca3a3dd178f493251b96e263ab582f3a4f95a763e65633c12e942462898be2981214314767df1962c889480e90f7b59f97d177e03e59dde7b98bc6b519f5707020c5575fbe83e2cfdc731d693b76bb3d1ae18e9bfaf4d1d6c69d159ebfd602bfc7c8e4ce7e67cf580b74ba1ef26689fa83c71b338cbac5960a0ab82b7562b93074de579b4c3fa2ebfa93fba8fb480f7b0bb9a10e11db1d2957ebe4fd4134e756d00c09cc23884f402650398aef1d75510649274f81c6d680147f67288179e4f0df4f499bdc40b15135f37d79cc0c0573629fbaa3cfea2bf6db4d27b711bba2e44f3413158f72cdab20dbad527f1959f16ac3c9e3c7b0bdf93fe2a21d2d0065835ac5f48384c485d80c3f997ff2585875e22474b0c61ab20bf3d2eb984f21a6d3a68199bcd8a31d36f977984b9a4b27270f5228493cef06e7b24eea4fb6523e243807951c28b322797c1a31b0e55a36ad7076131f265f46b53f614c0170a725748d2abd1422d105b41187f142189d085271b3cf58b5e0bc1703412a04a9d8dcb867caf3cbb7706e222ad006862a7ff309413a04770c3d1c1e89c1e80808d447a90f91e9686ae86de2d7b3e837dde3b673ff43662781aa4f5e773c76e58ea479013a7872a0f298e7ede79d978151e9bce69d7bf9702a606742c8aed7eae8a95b7e61e58afa2675bb8b384a58322b6362f1958229421cd906a75045237fd89371f012c68cd25723b9988fe0a2b81ec1d5b2357535efa5ea4273ea9cf33f3aff044fb73f5bf859d81d721df339583f6199c5cd4e0962132557f7fdb4f965ad88f3e0c344cc0319d6251e22ac9408cc122deb870366dc5019c2bf813c6573798664f1fad70da97e9a3182d6552d14bded8f74b531cb3884f2b6512300dfeb6b6e0937776b54c1dad244f4d7e2eda786c699350c0b7929fb90b1227d1c81969efc8be5eac1fd14df772b787ac3db440b388ef0aa6260302bafbb8d4d8d8ce0215144e51067a1ca461969a3a5866457ec4bf6cb9634fdb914066430dddd86638e95fd4f5c4fca29bc3198e0ca6b1e3fe665e7148864a1b74e18c1d6b0407e37fe8f701dec2db5ba9286ad26702833e52c4148a4674d722d018645d6b71f53895420467b8e26ee758e12f82072aad995c50ce1c1da19e17952347b4eb00b0c7ee3bb031f0067155fd88c94954680fd15745ea691101bd813a472ab475fa271a1cccaf83015d33b294ff7d3fb277856e361d94952c45fa14609cbab1a4d4fdfeb140767923f944a86721dfaebfa92533e0d4a549a67b666e647a486c8d3299a7fdfa11fb91a6636e15c9b8e9efcf129f14dda5233b4edaf419a73baacc167a7de8cc0a0864e48257c6445d4a7fb4fb6a3f56e5809acf3719c9f08c404d062877232a7fd73ac5337aee056447fb944a62b54cf08bc8f68a8785a1ae9f8da83e96d8df1037893c39fbd150f5525bea42d35c56508cbfe25a0cccb0178d752474fdbd9bd663c6cb81d5b843d544f789359222652580190f2c68f7cfb5eb047a3d3f4c94788a733ed42474dfa452232ae23004691aedd949408f44352ca24849b80554f1323a3690c1a77310d2ce82e3bed954b882a0844383808109836dc33e688081930e6cb843fb59942239fdaf3e60aea195241c463dadc77fafcc25bef4a8f493cfa42749cd4eb5742e35a2af2675929fabf4e3321c43dfddbf2e637f76b4e62107f8ebbf31059667c8668949ac1507a85b7fe9117c6563421d71200b3cb8bd1756ef8339ee9100c7985d0c14817e9153bac91b2085f4b3f7d27a7fd206cfc2c460f00f0c09eb219dae42910114a0a5362c04d9bb069de738d0693a5276aa3da1dc2bc25c863c7f75b62d297f2203bb2d5a2149429e863f76587694d7baefb19b4d549781d73dbb4b1ebae5d69c7b68fd0836fd8c5c5b715369c0a1ad848728c957177b8197efa71b8b5dad16c407b723cc5f67b45840c9baef33c3b44d4906a63c961d636fab4f3d589ff8a061dd75ee7e66bd67292ccc5a57863836e3735cb8c9a5a50bfded04e8baebee0eb3e6219c8b52dbf31d11d15f0468f3b47d879c4657c7f6eb85e7dff1340032005d1d1b5b8e5ab3bc87e6b76b625c6af65b5652b2acd010aa8d8d3af1da2bbf14441720590e210c4aeb3747eb0eeebcb6120748ce2bad015b08b63f784f50a80a3fb952442b8f408356a29947fff089008e4cd839f61bc3bd01a44ce5b2cd36ad1f33f49b65789cfecfdac57aeee92bd506198fb1e4c879862133b6d573a34c4d7000959aac7f48faec9742a76c87d22fc667b78c895b0679d7491cd658d71c5fae2d6f4d1bd7c76ecdfb8e354d5193fcb6e6127219c5cc70357f0def2fd5c4d8f19e88d43639800dea0c75df8daf136e492da7e89d5a8de17329df4772f77ca34353b1d8a40dad5d86422b24e437959e213cd509a2633204a5f745304107813395233e11f07c9d02823876f0727691f58bd4ebcf918cd4c151d6aabe629ec9076a504ec11f2437bad22d31272f2e1e9e19562333e85c3bfc814b247068a1bb23d59635cc1d418e4d6318db8444f8e59255f165ff9769e4d5163622c49a275ce1ba430cb26963ad2f5f4c6c8160aa9bb7fa88e81623ee2515bea70a0fd493c9fa6f97f5ad55146807cde6e9180f5705d53e1a75bd5a54672bcfb182ba38d4958d6c79012fef830f73bc5d3a12064725c03dc92578e78254c45d5f4f420d907e8e2c92901629683b05445c0743b12adf7c9df37abe72313b01ca20b1a021ee782401aaf4e4091124e4c7c04bbd6c5deada6601f6ca54c76773c9257a20b06446753d6b2028993a2f6710c37c2b9d1c754c5803e118b54fa457ae182711697bb193d4bc3abe7341b11e567ef0fb82cc72aaa65821731da1cd249bf93b30bd448135ce8eee715701d6be81d246789bb661b0a5dcf311df012567dbd344b83f6c17cd03ab95a5f1632d3db707a11ab364adad74311d2ea3ae64a93f92ea94e4307f5db76874387a6b7bd5032f11b9d70c819e910467f018e516328d311fdac867c73c88631480b0ef85db527b0623d9b25aa7ed9a21ca6370d25f8307ec81a1a6b51ec01072242900494b0a0e670e3ead19a9bd9ebb5908af48c4dda3e4f9a471c3d8ad89d8de5071e1dca569d0b9c02a8069bfaee682db494ae286ddac5dab27ba40c6c5fbfd0fcc962cd5729551a8f1c63486b717128ffbf80df91af28f43be13e3c619d05eed044b70878bd62690c78b03a9b5e57e4a85a42f94c481cae8e4f0a46b643174a34846c15cb4ade651b7eb242b8c72c3e1c1d75e9ec105d73f99707bde95c4a8470ec682bfc1dfe63083c3609aa431e9e63dacd6b86dc949206c6efd855346779ebea7ba7fda5fa2f989f17a7ca241850488250ebd9e5b67a78ee887933c78671fe02b6ecc2513ee9bc0e5ebf7fe6600918ecfb707c6e17b68c45fcbcc83072de39d87fc46a9cb4df3a96acbda7f29cf593d4249db433995f0b8830883c7e7d130b4c53b06491b1ad54b85482088d944cb21837d0f4fb4a7d2335ca2e8e71a588d5e56223b6eaebea895ced44a0e366d08b86a12636f927a12223fbfc4333d11426f3ad5b5b264e589e6edbcab8c0396f71ca1485cc3eb6385d3418f221c3ecfe4bdf4867c981f94e6a7df1b0d80cc90d5c2a428269dcf4d0dad2abb1280592c8fbc7f27b58d0f5ee08d1da7bc2e55a45ec9171ca622fd96ce82bf032375b1625b5a8af8b0d0f74e1285621a4fa126a36238ead82951976db9376f82ec122e300f902bf61018d3b47327b698e690e5fc23656592820e5660bd6f42b82ea0bd651de4439daf5067c61391a11f2983c290552215a58a4023dd32abd46a29bc830ff18bf37f75df59cb554356ae28aa7531bbf8f2b405c490293566253dd564137de5e4933899908dc8933696d34a2ba3e5566d46c4a580e850943fc57f082d1758e0e43ef7d2a8a305552944485c887e0a7462038c58fac7cbc7edf54f73d92a354e268d307a5ccad1a64078ea59c2684d470900b2b73973aee4e3a967f6cecc3e3925890d813b42488be345264149615f2bbd9a6c17e3b105ada4dda51ff27bc799a0edbf163a2b03342c5acee26b5ae6fe59123be48f18623c17e771c1a61c12565b2c329c7c44d30e52155f3bb8ce22b32aa3c8f3f3c21c8a61954f99329e3be2fcd29e0de36a96640609fa061f115e1362249b3d9d8c71f164c6f5b8abd7053ddd99e36e407110bfa994c77ab3f10144f7683bf5b6e80c8b169b91ee1c2d08fcda38e7a059fab9a26a23752a9b5459b9f1fbffcecc2e8b95afef0015db0990cf6cf9b1b28b372ab41d4ed12debffec1210ba9fd6d29cdb45a82a37f0fbb4cd9e9af8b6a23a94f938f67b4477a83571299c603ea56277c6788ce366e5df14d8be9d711e21ec6ce80e37089a907d41533a0f70a8a928562f2a00d00f865a14bfbddf283a6ff688e50a3da25e85a1addc7931d867d7db226ab8a93f90f44a9a60be9c8929051f38791ae7bfc80da0d0ed1d63b7019f69a1af078aab23ddfe1a26d06b75a9dac7c377cfd24b4915e236c0751a8e8667245f1797a20369dd876cd84359a2b34eb778c1828e2306aefb1948f42d3b3e2acfc0177af4b389a223c7e975f5320bd7c22759f2f39457d144abf28cb66d32e8eca13adae315d64155a8800eb1c632b46928fa5bb669429627fcfa6c18fa38687c63ab468036409def0d884027348f029382615cad6e2260c365cb2b18c043696f21a0502c2a48acdf6d30d740c0649625f3f6f30455ef26aa84bb8b619fc9a724b7fca6e8d3ba9ff4dcbf368eb1aa29585a1cbc3a165ed548cb8f7372909415bf712a952e940671f84ba06e7f2ab2f6316d3d11d1f772fbe98981f8a89def0c8875529c9b0550af7aa3e473bcf7488973ed307924724f515406ade0b4341cf062d7fffe98355f95d1afd57972f5f90792952d2a0359c9aad51a43875efa5906c573f22d17b969e006a5a47d91f3e0285aee6f516908aab443efb0736f4a405cc911ab1186d09c7ca2c44ec4982e408c1f429c361a18861b8874ffb187570b6777397e4cb0e4ee75fbff03e748c4c4da43af68a44672a399a8b4996c87dc708968afc4fd5af373ec7758f992c1bd4a15d65e13d2b3951d442a93f512146a7a55192fcc77a533643369c8a6f5d15b7244b6ec3a83017592a0a11c2830909b73bfb8ac5c66f6d1f696ec2db772d57d1cbcfd258d5be83a4bea4264cb2e7be1719b8327b85942c9bdc85e8b0fa15a2a775afb713d5f5c233288a7a7c49f6d19b2171dcd782e07f293942dc66b4fdd92e496dbdefef9093ab60514c988be7fdb4684eb34e911c02344502cc9e898ceb59b44cc6ccd69e188807db39824cad646f6cc2cbf97b5feef249091ad6e9d1158271782af4be046cf43310e2b62e52c4924c5ac63a44305f5ec5d8176790e0e5cdd3c6d7f7f9b792e80d61c773ab3292f1574dc748eeeb1e9218d1ed7589a8ebf6e4d2cb6ad7e5e20ef21a069970d8050ed39338408be4d4371ce9da085c76239be0ab6595c5b1b2c8c451c82b9fa810e01d20cb23933ac66a92f6c3671bcb99b84bb692fcf72c01ca3d09958896b5d94af4654561fc8622a3513e0856a991e21cda9f9c605ceb79fec79aec2addcf4ad470342b3630ef0eb9fe0642460e684431c72a6e09904780a67d0449580318351225ecf9e38a2180ea27c70826d82d5135cc31a5d4f55b813e1ec2b7ee815d087ab52e57fa5f710e9bc0c8663d6d499cccd1d948d5ca57f2db855fad311c6fe76409a7c871b74cd0c8db560691c8949feb38fc8947a39c819232ce28d3026959b1b1d32b3603883a8c154d7fe3d468ad893b9a456b7a435123770ee1fe9b11acaf6632e83ad3931a417498a646da0c1011a0db49971ce020270d9bf3c0920f9a6279659ad08a7428c282fdcb5739d7b5a9d0bc8c55a7ef215ef1cb8d5ce28c8c9e04babdb1d8d445d2186e910a1d56262cce1156d6f0c19535b92560e28cda5d4d6ac103c7217284958b7ada78d2dd4a6892b320797b5f395bcc5852fc3c59fc5bfb9545d27e91a5187cd8c5183f93fbd16366721a1f808e0f0f5d69cdb6f84ae911152ec2c49f2ba66489e57c3a01808932698a15c7b7091b57d8a389e9d92f88acdfb018b5ff1396af3586830e5050169468e6a0980be1991c0d8a736a50ee06aa474b1856c90263c7e03b8255723b9eb50c037774e524747dd2022b1507091f9ca83717d690dcb1c8d6e0805b804f857728a98acbd19965b6aeb5a8e548a81b39b6fa427bc272e45d6a2ac5037796bef7fa91f77edf29ac05c08060351b359ba9d318bdd487f84962dd9d8a85b67a7485d16849f40cc4f0746be2c818acf874d4af13be68727c83ab3ffc34cacf4b7c36ae3ef635241f8f7cabe71ad250a1fba7222493ab31c634132047f29f439536740bc1f2ef8652a6ca590c6dd63307d5e5ad434bb94a7abe0a8086eefa4767f1b4859ad5562691d1e6fb2396ce0802b5ae87bbc77c616c3b5e162700bdfe551f6945d183236e7279aa37794ccbe84a967d27f816170abdf5266416d281443a89fe7f00af0350fcf2dfe609a5bc79e894f18347c36783f0a2d852d705e9b0118babcbd0bac5c1583516d24a67e2bea6c114f684b4a91a00f2c6cff7a025a527d5a740771e7677d539912ea2ff5eda4e48753ae2fdafc9cead808bd057f090c3b01c65f128148800786981281cf7725e21774bd21c9f554420eb9aae5a8dc83576bf62212dc2faa44daa9e3a012aa51be2444f6d59f1311c4fd9f2aea6ba3231e709e18c671e872c9113136bc7a3cf003c022c78b8401bc7ea388a3c271197d93a0e1cb9d07f70444f598fe3e5130de072859496bf31375cfad7635560ab9020d196b2deb100ca82c9f2bf3f5b3fbc4bc5ff716960a326dbbc07e80ecf9f86e4d666dda866fc09189846b313699fef171b06ea58f12a71e7c910ed451da62e465922424eb6569fc30570b586d5b74f08c27b260419a7649eb6e5e8d08d2776e2c22e442f43fb1456c07080f55fa91665bf2171d980d9f506e07d60f4649b018e601c207562b2166ab02b85ae2ec5bb4b905ea9095f1f2089d74bf1cef00038c9eed9674befb81278b6e8c368e5da47344f62c08e336513f4464b9969b3d7e0e1ec75d9c19610b218b1faec21fd1db80cd6e378257a18298fb21040d82eabaf2e97f10802428144ef81a44a379d72997e6a6bd081750ddbfad9b1856796d34840639ea19df863d05771c0612caedc1b1be0c505061130ea11cc41450a18dfa98635190544049d6d95b332bee9d51b576fe7c292f7a2bc5a90e401bb492751c86a6f8719a3e9c785b4583ea7ac5074b2a4705b0f9483f7a0047e0f1f7148b198b597c45bf03d2aecf640909c0bd9be6ad2e2876dd212b02c783d8be60f135cb17915b5e4a4d1284689b560aba036bbe6779a50bb32f4d3d7c7a3336c3bb31432fac5cc90580ce988461429e1f72bf18a08b86605fcc63b29306dff1920712b631ea0938d6960531ae158a02431f4202eda981af9e84823db7c7a8c0eaa21321b824594e85b6ce50f2a6d75a9d0231eff26d08e087809f0d5336bc9c6f56b1ee4f935865cd72ad78aac755223a63968a014b5143a1449d690e0d43b4430fcd2012f703c90280570977617c7a00046e40543224ccae220201845b4bb524c0b3d45b38ee6a3529a4e95c1f286571a1eb08033e94bd4ac3593efe851fef9472352400909893c1111022c742900880634e18002ae44434272e3c857ee6efde5a4a827609021bfe264e9dc488fc0505cd539af644d855ee88156148039559221dffa8f5b125c7f256881101a1fc93e0ad5ef8ab96ca5318399ec31f2c6a72b2e78fdaf2d7456402d394dd94b98981c6ea3668c7927406763ef2e40010000ffffd3fc536efd430000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.svg","1f8b08000000000000ffecbd79b32dc9711ff6fffd14a5a1cdf07630957ba6cc016d11345b765f892d965ad6f53ac00c161b98813043109497cfaec8aceafb06efdcf3802080208240bc78b7ebf4526b56d62f97cafab33fffc54f7edc7efef9cfbefad1975f7cf2117cab7fd4befafad32f3efbf4c75f7ef1f9271f7df1e5477ffeeda73ffb67dff9d77f31fefd5fff65fbeae73f687ffd6fffc5fe2fffa27d74fbf8e37f477ff1f1c7df19df697f73fe55836fc1c71fffe5bffaa87df4c3afbffee93ffff8e3bffbbbbffbd6dfd1b7befcd90f3efeab9f7dfad31ffee87b5f7dfc37e75f7d9c2f7e677ce7e3af7efe03806f7df6f5671f7dfbe9cf32e75ffce4c75f7cf5c91b9f63ef3d5fcf173ffbfcfb5fb56f3ffdd9f7bffce2ebf6a3cf3ef9e8df7cf9dd2fbffef2a3f6c32f7ff6a3ff78fbf4b39fdf7ef1c94700e21fb56fd75bb7ef7ffabdcf9f5a6b6dfdfac98f7efcf7afdfd583bffde2475f7f75fbe9e73fbb7dfe934f3ec2ce3eeffff4d32fbefceaf31b7cf211b6fefebff9caa75f7deff32fbefee42388beee7cf6f9ba7593ebd6a73ffee90f3ffdeee75fffe87b9f7cd43f6a1f7ffbe9cf7ef0e3bfffe90fb3e0ef7df9d9e79f7cd4de6b80747bf3bd7ff6fe7b681fb5cf3ef9e899d81a036ce07d07b5062cba91c2be1ebc3c83f61674e405c81bb835501fa8d120629040038d41162d68fe8c7a7a0318e0f9c59879bcbc59b53ffd0f7ffbe5d7ffed7bf55391593fb4ac53ef3bb235e88c1b90ee40d6404837343bf3f1cbb3f07a5160bdc85d77eeeb45e1f5e29b75f893f7e9001566f9a6d218fa261d7646687d43f39dc4eb2e409cc2b691f3ce422d3a6c107242e7beb1f75d1467975a975d915a3d71b73db2fff20900d00e9d603e0324392b9fde790f92960500683fb3c4e8b13b46eb9bbaecab762fcf8256ef59c0eed9d91d3609ddd7fdb75bfd9fdd537f9fad76d14626475e59ad596013ea438c9b321fa4d0ac4743e5e64003541b748ba312c8d450a101b10f416ec0462768e0a69960c3c3191a3027cd70032218d0d11bf4ee9b331fcedc00389a250120e190d006047270de21c8967303149e44d71d8f4c84f7c6602db00f257d72e8c38d9bf52ca3f7268a033a4323b3a312d8bd05f7065d868ab41bd879c3ee9b743b6f6007e5c837046d0032007a638c0d430e0c6918bd917a4390212649868775cd6b33cfec69ac1e7d7b20fef3f7072239400d04746900a6472528a9396714050eee941d6b879056a229ac47d621bfea27f488237f85cec7ee3cb84bf3de0fb4ba3688dedc69ccb2ba9e59e0cb738e627d3f13080ddd5a18540681fd60d3bc368168a131442933a0cae0a85f28dc04fb1350975963121de839945d46650d8655d19767076e2476e495dd9b8735311c00804dc50650d2b3d100066824fdc424bafc019c8d476fcaf93eb71bc2f088a638323f743949ece539045b7e55d7de5b245d499f1f451c00ae2da20162ce501f80860d9df2f3a37e70f6783e66d75939219da4948311614fec36b20412ca3abe3c73f227e83b313630dda14bf606f09e8b4e4e1adfd73b0ff8e4a73ff9e9fb6c12d0e82214681470e495ab45d1c4799053338f23bb3cba354c2e6531107b03403c2a81ee8d8a7a1c8778cd583d8c66a239613e8a11945fa91f95e89e849333bfeb30eacd398ac999f53dd81a396585a4092409813435dc001cb397b1a6297489861c3ba0da53dfa0b3ec91830972e4bc2d1e20ade616b61bf68324f2da90725519abe92fcff91c080e83c86b4b1e834c3bbb36552f0e9ddc2bfb428a9eb5fa2caf68d448932270ac7c5e9eb96604fa51894ed804a9b9fbae0c39158e6482d0215af555571c2631bba81248528c12d06288e788231f92f393901b0b3d016a8caba44743ffd32fbf7a7fec49d6129e430a8cb2630e5317dba0e78ac26b899464b9f880f5fc17efe56aaf9c87a849121451f31ca7282e20832ca9a2d3a8e54c99764d5e2f08874031bf688c98f33a06413455de93b8c4e228220b6a9c43187008e744556899c78dc48a7e6e6cfde0e8ed46a18d124b206255058a4aab6a6f37e8bf7cbf4180b341a2d6c4e1c86baedf92c3928b07f5cc9e875915bc93574d0e8824378a641d2d063934513825e8c8b4776a44d6c269204cee3c122009f53d9728653ab00ab1de923301450c491ede11c7aacfdbadf8afde6b85fb6a45d6369cf65a2b8df61c0c44dec9726d02dce9027182898572be76db3d4b45b0dd9358baca2eac2d8c77e35c1a6957ec4d7bec4c9c7c774fceada03b6034ebb6af82dfaeee7f7d87242efca4566be006ddf4d4ae9be6b2c4bab173fd363ff379fe045ccfed51b7fc37ef95c31d2f6a4d3a8abe63d20af8014946095573c1c0132136ea7e1a1fd4bddd3039614254f4717dfb7691b7f791b32e642a98d08c36b25383374139851ff4cfb7decf8462d59bb9851d79019606c979cc068a37ec3128ef9bd59c0b3bf222d088a561af970a631be5cf99d5dbe57ffc3e3931aff293e8016503df15756155e7fd7af0767efd2172844edc147321226e18891092ebf910d3c9bd9916f72e843820d787d0d3998e4c032837ccba104ec4598b91e74cba100710eaa8125ce154849767e7686e091c93fff66896dc16416716c976b99631e4567304c106f55cc1fc94c023d394282d911c73d6f769ae26b54e37b3684836b2003138ddf8edce81879d6305dc85e90424dac172110d3d0195774b28c64a9b619c0f88113fd0ef262da507388172364bd3a0c3c89a63afca7bc4f064cc5df9f0c9a1bd99144c83212990cc6e5aab1389242ed18196dc066d0b3a721940f486990109ceef0a2e24f1165c881c21eb09b7f3434f6cd4bdb941ad9362bc17654bf10579d45a7ad85a4ac80abe09c6a1da6713499abb0d4f9ed7d58f4a643b24db315b26170194e4a00323fbc338eb71403149b686b9d090c2a82fab6db9c455dbb22b486d44c8ecc94ca43097828c7b1fa6da8ced4864944b7f08e4844898628d7bf605584348ca8ba7902152136320520b18c18d9c37f47ea0f79692740e04f65e6f26496a7530b4ac15761889cfb8e3e18550a259a2eeb0916955db28fae9f0005cf0c35e8e8e8d3d3680ce27916fd1f1ec9b81d62fa193d9764be8926c239fb2c7cb332ad577f92260ae2d91c29cd2be9ebc5d11795811ecda8cfa9efd3165e68e99b5cbc6643b4581b24312cd676f270617389ca47e270ef7e4905da3b1f251895cd927ceec436b4de807834cde9439da00c9b1a08d300e626d58627f2218a94f8a3d904db0092920c670e7c68a475ef31def39137548d093794aafd8cca5a53064843be53a1bb8af36bedd35fa58522fb587d809d4fbe64087445271a4e01e255c8d945e1de9e0ea17695afd2287635d5b90372f0a156c6c72540225d1752f89573b4cde5d90288932a77a4f79ac89f0a9280714fa446b94621e5294d89575db9cede5593b37ef7024eef29452925099474242ed700a51c2c327126a14291350950b4487d57240cdc21aa20caf81d423af0a5af7adfb5865bcdd89f601e659da0ac19dc55bdf921dbb16a4e3ad344c45739abc55f0eddcfd03b92714ef25c4772e513c8523771c16d08ce3889e62424f91a269afa5cd1b454a4f9448805b00b4c0522fd440e822d424e2149071be9ea2680ac3a6954f320e937e24e15a644e38457dd655a34ca0f6d2e400a92f0546b13c5b2ccf653ecaea3fe5672fcf5e8364475e53fc37d35cc4871835351a64541c08c3f2bd41d697682573d6943a632daa1dc7ca2f65b88b57ab2e5e5db2b8d8a05291191d9908cd165bf3e0cad45186e53c0f1a9ef25f371a267d2a88ae5cdf1eba783cbba837e55e4b9be4d84431d891820ebb1de491d7c239c23140a019a6489a638939e089ca532890120607259a60e02102eff77340e9b572a5c885864fc77ed40f0a69ce5e8b0031b41b6cd4e504918d988fecf514f8355290e5b1aafdf22c2c557f257f52eecd409b860d4f86457e06e4a291dd1adcb2fb00bb0f91c2497830d50a8a858b0031063ab6703cf2ea442d27ab918d55d0dbfdfbcfdf47eda1ef96f184b5516c7db256b3219d0bfd4ade371b299a841d5aaaa694fc12eee64b897e199282c6cceae51992b7793ff23a358a5a024f1f98c0b0070eaa47da474ea27cb7aec48d923f76aa37dd702410cddf2bcfb71bf7be58ce74696400664d723ef629fead9aa0ac9ad4239d635a35c96bd60470d60465d684fbacc9ccf3e539e76dc93b490c25ef904c7987e255dec15ef20ef629ef182c79677dfb40d7f0e33b653c745ed25c720965de13e151c009217b4d0d3e0d7bdd861e7e46c77dbdfb76299fdc4d395c1249b83607de40f00c932d5c4f077ea90714510f44a21e503cc0127ffa83375a61170c0f692a7d4fc12f7a94f2710ff76608a798d403083dc97d5f2fbf5dcc9fbf5746e86556116b0c535595288c3c11990f4e5028b02be402a05337ebd04a63dba31f53759b65166716ae4f80808fcc23132d676432ed918c1b006433394a4d9e7f26f8d6f95d32185d1cbd99d723195369175a1835307916af6a65c174086313c5268c4f0c7d2bc52995b6fec80be4f2645a86a05c32216208f73204897a0b9a3fa39ee634a5ac9a8f99c7db9df9dfbd3f60fe2a251b79938e072410a85697ae1b0b819432f000e0894da07be96d73ed4cacd342b52196e8932faaf77a4fbbb55cb1841a831d596d716c8ade0c6098e7a22b233c277f2466d44c3400ea2db4f4c289f869879485122326d86d91333db193e7ea3a7f0b5122fe01dae3a91a92c514c3672d6620234a288d38ac6c2891c82697af64f9c9880047c167f303054af94998707a48b59ca6bafb46e847d6f346534794a98414587aaea5686d374a1c8c5ddaadd4f2d0a9dd187d78e42da14379265ad6f0460c0342db0d6ce48296d5c88e48f1114b198a550f208091300698e080d22e33c11350f50bc400cd4e091dd7a8be3c1b942ef8d08428262d1f24497975091d8175cd95a781f14889ad11f6bda45ec2d80112e352aeb82acd13cef50e9928209e37bc7b33c6b10a7b9b06fffbf7699068ad5561d4c8fb4629c6a0264af47daaca453713dc817a8af7a5855b6fbf3c334293149d4077d5a459887ddd7cbb0affe25ec7bf344ea0d1fa04a3ca72b84fdbe26c2a90f8a86e28e1b7525166392de96814c6d404a1986b761977a0698e2b686f5c73289120265a2d552b0e35ce2669bc3c9342538713c436b514afb181241b8b86a845bf0c7e94a9c9e1fabf91c2fcd849361538ccec2945332fb8e6238a4e358e4a8047f36910a1913408189e999c4e0fb4517f713f68d71a8cdc4b1a03049a1aa75e4a171dea4be4a3c90e92ee40654069acfbe93dfb2357ec9c1e3953213168d4124b634abda647d0826fd0cb2ec936aad470dfa0b3657f634abfd8220100721f0bfdfa215890cfa725043c2101360b3955f8a049a98d019e30715099feecf03275598be42a3d461693addc32f3b7bbe83b775dc4744754e28974a788dd0272d929e31b67e38946f139333bd59377600a8b39624cc989467ee1d34af30d92c9e12b9a11a52305dfa499e8de3012e91236753fcd4ad6c169688c6968f521fe6efcdf6ed85fde6b9e2f201198a248910e945124f8ac8a5df25ca99c5f89ab6f1178aa3d589dfe87bb82082fce80b9685636bf94bfc137f2ef7d0bc3531fa988ffea6e84425e8938e1081c00d69b9725ca1bd1b0b5f4a5289124cc404d6960ef8d3ae47035153fcd2547ab9581b3c83cbbdeba5d042caf045c32601fb3c48eb141173c62b26e4f117bd12dc845b739524e83409a453fd5e028db3c6ae3449d29e596186187933f15dde6b4d2d237e4e281a7e85416194ad2309ff0c8bab3bddf4bac97948d9ea27b2ea5e735ecdf188d6b8cebf91c1f747fa479fc97efebeb2f5b1b4d2dabd3caf5b1f2f27fbca7970b7edaf2fc487a609423090873f2b0b5f02130810ac694f1c172f51fb91277dc90e5409696582f8549ec565f64076b696ead590a5080234b62c4aaebdbb5fc9fee161b5fd347289a9aed398739f0fdbe34d27daa77675f72df55ad39241cf2eaa42eb2af5cde2e7cbfeba2cb9c435a6b4b91df2f978c3997dfceeef9fdeccc7d6657dc9545770f6aa8be03bd7adf287896c0a8a7a8edc0e57862b1e7e2d9378798267b30adbe10b56fd6e82192f857bf0ea5eed550c0f71bb9430f4b60f3ab49f55fdf95d3fdb59cc815f8a8042f9b47a3ee037ab20aae85a838084d9b0cf75efe13c18dba14d0535ddc19bc855f4b21f3b514e66a46cb4493abc1e42429d852e0a812016a51f4686674aac3cb734a89cd4ad598cca5a724564a62c3991f015cab233ccdd5d16850d9753cf338329d8b37977387ad15998e89aa13625243cbb646b6c24e7be47ef3d77793205ee9109a58ffe6e858d723582ee4552215713674b6fba894452207e7a69eac9633977748c8d02a1fd79ee9169c8b7e2ee485bd7a4a1b99825251e3b45519d83b2c64f880ec8e0f91035ce4008d739dc7142621bb08b3ca3973b5ac7e3b802465b0ee09366e78d8b4de958aa92806b97e5361c8012e93627abca3981e936212a716c54cd7a53eca1e3029a65f1453831eb96af578a51868667c514cd14f528cc7536185f0220b98f915c54c5b1d34ae4c8a62e01b14038d291f4a514cb6059846382f4af1f830a5fc9bc77e829d9a78bc2e3bbe70baf4c3432f6a292d107322ee84a290383d9171f650776fe630dc7315ef2955e6529db975887d15b00888336b3d92cc8c35a5d0b270444a18bd407839d014f26f9953f9b5bd231f7eb01efccd5d03e13277e72ce228173b5b9a67c7048597eb1f96c2025a4aa9404a43f15271da653dcbda70515cd15f892ca58e23d982f1286a875c48a608803393c41c6cb13047efd3784753dac75271e574a79016844353b2801841daacdba8be15a52ccc9fc812d9932f9796de5bd2024d0b07fbc452d94cb252e842c8f014907043a3038da6c79ff586c0632dba47d99bc99a2b97f09a4c82d447d2b8108dd5816ff7fbb8eb77bc3c1bc0d6c059cf9547caa816bef1b5388049dd793be77f7b872bf1620810537a3d39c542a8b5b184a32a70182433f05da7ade148485792520abd2976946cc56bd1a27eb2f583a8b74b6e29b22b338a1d8e4b66496e8d29c2e7647cc5ca100f1485e75df5fb125c34494d64877e79bc009aed46d6fa964b392e708264fb7af9ed32fedd9de20916dc632f57a55d20c9da76116ccc7d777e3544764cb649751b08b30f7907126eac7d077935599ad10e8cbdacf4e5e3566a68d8639ae271f75a3414762980c0407b999da59cb8f65599b7dbf03f3f0472ca9c62f69e13647553b213299fb5bdd4427db623df14f53d07b86f627bd9928977bd7a32685fd9bd5d8b7f7f578b0bf42a5033945faa05ecd6a3095391b59dc2b4835c8326fbfae6eda25eee67cc6555c84520c123b09e7d733d8179f7a4da9c37f14a73c02710c8be3e78bb9cffe57d51e0129c05f32bc08d08cf1b68df04e9bc11e09605832ad79d7ce7edacffd73baf9cd559177521e8eeb9d002caa6ecfb7af07676ffdba39aa6b4a5bc91dbac5eccea62c7aadc1655d9b733fddfdfafa3ac5ee662f7003b2693c5d89477d235b81c7db752a8c7a660fbf5f2db65fc1fef6bd15f6d01d8db0d04b614dc03fb993fdecee3ff7c7f03c1a502e2f29063dc72a4e5da36e07dbf1ebc9ddfa777b209ac4a79f7d60f0b6c84cd26b639541680369ef65998f008015b39c244a3de0b1271f91e58b9a14b44d3049516a7311ce5a6436588685eae21bd85d0c1e5c5406546889ed2b665d11b101ce57c901defde827cfa8c75f121c93ca0e361d3cb019bcb849723949f4ce044a12372bd2a0b75c7063563babf3c978f2ac32173c96eaadac0b5644aea744a9224f181c14d505a5e09b57e273620c106d1c7cae7ed5efeee5d2f73bc3abe6b134c242cda1209c6d42e0f255d8abbb55243d98777826264dccf1a62423943e960b2d97825aec488140ea4fcddb509cb29182fcf6ed024d15a39294599cb9d7164fb83a55c806b494749187a52b783cbc3c89a4c9bcb61382140b929701f2bcfb7dbfebd7bfbd4e5a95a7a1838d496e7195b8a5dc3311af1dc197244b948ac7d0b29ca39b5b54363fa482f2fbbac12daa80d1c708ae21158000ea437e7a4252d01ac6cf7935e4416bd9824ae2ac37f33e89b631c9eb409e557503e0cf959482fefba905ece4ace31d0ec49247bb71fa5ac28737112072e97bc0744f1d9bd397e61d0901aa29092af10aae021b57e66bd0157bd915a184e4280de53c277f2da3803a08741e97e9b94693441cb726b012e97c42c87f4cca17b46ef458779a51cff24478621e59f608715595a958921a7454eb85eb452a6b0a49dec9af5ad731f2bcfb79bfff91d5df805c13d96f347bcf3a0441e414d004e6139825aed3c116e4e38a8c0799245d6b6fac8d674f0e0165adef3d044e564f70dcdcbb44fe88dac8c4cc5a3a6d3992da73369297479494db647def7385ce77c5cb57c79ce328b308427619837d718e8fca49cc4a4a5e676f066b108ca70acefdeee9defdfb9775fa66982d6cfa0d8b49f29156c487026aa3da6a7a95aa3da8b947c372547113b121766a2590ebd70df53584f21f55029fd46349d77e2109889b28895a67faa3f436781ca5615e0e2a2f888be7ff090e9854efad6722e9ff48da380468d1dc21abba427c5bd365165c9d0a19f58ae85b9623ad7be8c5b4ae4a51c66d483892b51e6e31b99967073438ada5b7703f4833479ab25f99793b4140faa3bb988dcca28979d28d9e2728d9df368b9874d96fc34e7118dd99e9c43b8e61036626d24c91fa70bfc2fcfa172193e2dfa61a5fbc839146b0ef19a43bee6103e9e433fbc772f58420f95cf1b1c7cad896562cade0d5f7722a019482d847a1ae8e15dcadbc54aa08e91245e758a59c75c8a3c6cccb5a11400bfb40c3d022d3fba738c79dd9509b996c9222c6238cbe9a3e8b8363c9513518342e62c03652a5246b2b2ba43b55f5363dec1e0f90e2a0fd09877ae0cdfaeddfff57eed3cae3d0893eccec4a573d073954d0261b2e3965491376e293832c4794383e3563ac614e2a3ae4749da5606f552354dc714c0588d26787906b4d5e44c7001f59cc3cc0397dde2405f9ee1546ade6c3bcb6afbd499ad97abedc2abed2be7b7dbfe7fdf3161ba74c98c4d3aecc95ac94b35c8708db59eda13ed4a33d4dd14e7f40cb73db95aa9753a5d7ae595d5db35f8f1af411b5568d1c69b59fce44e217e9994129e66cd76aaad7b7830bd4e873ea743d9de6b3a70b4403fa68aae775d1e165db2136382aa0354e6870d54522090ea1983d3808f4c34a75eca9ff2d29e365296034a2b3065e1a8fd8911bd5969e5bdf3693d0eef73ee17d4603928120eca53f2738b1a03baa8061e08325f3cb4c750ca9cd51998d9eac18b25fc2e79c3aceb03d9f67e5bba2eea8b049470542d12c25173cabcad4cf6437c41219f3e1dd3e61c1c03ba25d8c57362ea9c2d562ac844333a9ca92594f2851e75892fa5b5a111e5af708ac04bedc82e669e988ea325a349b02bd33de9309cae9d29272550f255e7bc1a46f36e4f4e51752d78c032e181d68ebdb1f27fbb677efa2181812f81815bf92916774b8161e9af68a23e2af73b396f0cfa8dc1e851bbd4e66c804b66b8fa135f67434a64326671ac4b78d098c28346b9565824b9c710b3253cc43be121b476adf234a636997acd895e099e2c12d9e95879bedd0fffe1b1d9fb15232765a6fc99b28ee085ffe09205315a18ed9ef2d1c410dd669f38e209bd1f496273795f5481fe2ad402e184c9f6164ca65aa6916c88f5e526bfa8023151e3e9dd6a7f698ebcd8a204f22541c4932787ff104cfed9fb027f5c7a41a516a087e66c469ab923959769feae3150ffa589887da794192465bac9d19af8129f937e2635942ba9fb19f04005f3d5ddb271a94bcc7a43b7f2af20c5c29bdc6908586393516234db807226a89dcead0cf709f5a07624e56ced43e855dad10b0df6e4cb561a6753de4ca2769f3af7a6490821f5d9ec6358609c5b749d9a8480a3aec8a5d5d614db159aa20cf3c4703002a831fb089127ea495f524ecd4ed1d4c7c48bbd88bae6584efe4498587b2c4352fc5b7b5a24d9458982e05a5f9678c14bd8cd05c668ac0e7bbb9fbf7e7ff0f58226e543cd3cc755820a9053c089eab56e60a7e5680ca3b647921fd371cd9b94af5e3ffb21386def2cfacb12624dca815d1baa57e66be1c1ae67969c85bd5de9bfbd07a5f14ea5043921fa9c701c0b4f2f3935d9b0f701945cdb2f9a85939c2a3441e920709921689921fa94471700a9e514705f45bd5dc39fbfaf88bbeca01cd650a0e24dcc0c850bd1f48d51f67226cb3e40dcd7ab6f17f07777810e5e5d3c7bf854e7d34418b80107ec00a6a59f2e3c5556ae5dbab5f249ddf9b560db997b43c35d63a12e67d8af6cdfaece2feea6ebabb5ab5353b7ddae124262d75ec138f6b0defa66a2bb746ddc6547a9ae809dbbd62bb2ea809a2f555e6f57e1efef3c88e3eaf214af61375edd11ccbbf46837509bf3ac545b3e137b62811b13ef5e621e14e8dee7de569303710a740dcbfd3541835abb85efb50ee18e578d91f755f6db35fe8f0f3bad50a5c0166c496d7e02e9ae2e2d30b6b0458a602910edebe5b7cbf87f1e32f79c87a47ab048bb11689b226bc781d3f1e0a40a6d91c3404db9cc0bea5eb7d4bde535a89f9013bf3636e60c222de7011ab5e24bd8ae659a763cb86c3020ad1240c952a0ee969b4d705390fa9d4c23af29c2463f384114b8b4ccea96d3e7aafddb8dfe7fef10ffc5d46afdb5be95a5afac6dc839be0fc0c1fff7a8f74af2123810316ba60d0b8fcd2ec36842d0187a35287f6b6db78aabbfe8982131401aceced9c5abb78e92af842f150bc998eb28d9193817d5ec7cc51a845398ea5e8e515ea9f733bc6edda2b7d9e05c1dbdfa6b5f157fbbb9ffffbde7dcabfba75233b3a3120ae513d09b008cc4c5bcc41cce45349705f43293d70e1fa2dab5c9c24d290e4af2a16844dcc4e7c65126df73c962d2d2c24b68e95b35a2bef38e8750cf6bd3de9b591f26de547c04f4266a7bd6435456988c62af8a4db1f67ae427ba5fad78b0d3e14f7ef1697f7fafc3a33050f932dced58895737fbded47923f29d04b2eb53d0b47d3d78794e5417a1475d055aedc4c2b9b726851fd01977a71c5742eb377483b9f9063a54b485bab3b27adc28bcdbc001d82f45e8da073bafcdb44f0539e35290831da5d8449016a51696a115930acf1bb254bc811bc0410992a9615fdebfc9e2d14a4f9e69336eb9d807e8e069f7c613087c53ad8d2372acdd6609d3a485eadce85bda72c6c389a7b6bcbcb07164d597d4f334a51e9fdaf2e8ed555b9e5380a8b6a997b67c36f37157d17d57c5654ae4689acbb2149d1f5caa372f5789692445df013057d4440b79cbf02888af5a862a2418a8dc28e8e40eb5d82926273fcd624bc68609a03ac55109249f3bac496ca8dbe5ebb31ca563ba319755acbca46c8bee47e43a04c1cdcbe95b74e8941dfc1095a772fd9072fd60183c9d466267e696b530a5332bc58f3c2cb29ff8ae9fde39e54149cc78044fd1d796d4284be8298f41e83bd6fd8adf93d4b0273047e9477943aca04fdabd7e57dc21c11666f3fdecec4b15b5934f9fdca3b6ca2071959989234ab35f2165a2b6f8d00e58dee39a082925f71a37ac9d717c00517ff272baa3553c956b1caecdfa42ebb31e33a71bda7eb5f9e59900eaab729df2f25b696451c1bbca91b4d83b8c60addf00bdd70b0062b3bc4c94a32ff472f5af77a177981f77c54367f0012c81127ac7919deaa46395ff78e4e48ddd5cf2ce51d582f6b513ba36eaf65d839a9126503d156053ea27f3fc4d88f5bb6f4c56bf00a29ee6ef7c3b7fe7d7e4f80d8f04d857598feba9f7cc15ae481f568bf829081b119d7345677b79ceb9aee1f9e372a6a1531ff9586431761f08ea123401a6d2e9a844cec7908a4173ac006bb5d92285cf398aa5f79fa98ad6611575478602b61b87d4aefa4c94dfd58dd546a2801b391e911f69b4bc2aef09ad6e8a05b56e10b8b099dacc8c98ca149d89e6652a7019916b3e503f2ac1d09ce90960061343ad503b6015bf2bb1c8a8dd068ad3b6004768b3e451e6cd518a6fb955d094168c53c30dd0a7ca3c2b86657a341d4a7d31265d712b2225072a37b1a254962dc8e60e8fe97a4f151f627e4a2807172b4bd857205a7daae2928f4c9d5c6942a4dc1b07d7867a18aad05c7cb84733c7019dfa9386e78aaf4d5cc635862fcf9ad021e010c0668941c99a311e94b3ad9419bdf65b973544a82c2c5c96e068cc3412da24ab5528583aca324d7e784a02b9022f87e0a411463ff22a304378487e37cb7f4c89fe3e25be3a88406d4ab219920cca52561e59ac03a1bade66f80d669bae7c944021ca5ab1dec15c4e31e69d2bc39767a16508a9440149598690d25fb3e9305c8690da044e1af30e06cf775079882c43c895e1e376c6dd8cd31eaf1b7da32508ae44f98ff75abd79580a1c4ee5988f4e4dc81ab90f666c6a7a9aeb91e948a6588a38b5fa08c0e898d1298ca67f34f480a4ccde5cfb965c3f8c6b72d7f6b8e2b4f965673eca21b633973a3f4c0a4298d3a9d60f29954eed106fecf5d513031eee73c508d4c6c223e61efe2d9bf5326370601c79950411ae8d739ee61ae230bc76b24e6febfcbd2234161694fab2123eadc95ad59b1c08d0b03e07123d9467a255c6683ca305769e5a17c378799e9ae423a0d542e9a5b1e3c1a542329d992511ccd07a64362601e65b2089abb33e02fda9d4695ceb7b324495e65cdfdf100eed1546b071f9ba0e706c54d8bdaaf190563e7d9f56e29290b536d1c8a115ec23965460e54dad018d426bbaa359fd2ef721a1918b4740828a0a1e16b5fb13bac7dce50b1c9b4405d08243ca199a74ba7813e9c1338a8696e211a8c7983b8809f6f24346a60366bc4e6d1567350796976ff6a1cbbfbf69f932f69c52f6041071bad361c98a267965db36057f794e7ce6e807175ff62688cdd94756ce4b2b45b6a5745dd11d7ad66db2491904bd05e291d7fc7465f5b8bbbf7bd7dd9753b69036efbc5be47a679b0a55e8560b391d78d762e89d360bded7cb2fcf9aa219fbeeeaa5ce30b49d2927689c22be4f8f7ddb5c7d5fef3eaedcf7eea139bd6aea1364c96686a7526c8036b789899f0f4385669e9fdd897bff90986499d3e7f75c0d5e83ffcc39d6d71ca36b8ee135c7faeb1cb3d73926d71cebb1e6588f36e718c435c778ceb1bee618ad39e6738ec19c637d4ef514cc26db91c57664b11d5c6ca7765a87426db42e8922b32190646ac97df8e23edca2f76f729f7a7b729f7e719f7e711fbcb88f5cdc476695926a346124e8268067728dcddc8f323841c9f0581dd1a7101f3d8e29cda7f0e7d80c139e756e5a483e9b0676b2e8513fc8666cce4684554a981da1543e757965e6119a022397bca29253259e5438697c56d11c37ef71b843abcdec28cdc146c5aaea7cd43561d2653eb080da5a98599ce60f1c23936cbe7f37e32eeb7885fca18e1b309eb9fc6e5e5eb6fd716edfbdd35ed845ce40b549456ba7392446c0e98be3837c59fff9b2fe57a8658a2807c1faaa12508a965a43a5be0a9d81b5b20371c6f5947195f4f25c1977f7832b5c877babfd2980346a7749663c13c6f31101ccea1047f9bf67a2424801411f28f25471e80ada00ce5a64ce8fbbe44e47033d5eddb9a3b9f0061de8b44e9b729ce891dcb47e879df93c7f02aad573177e799e6a6ba094f2710beb8ffc06b20677da17932ba2a3530515733ecd64afa8145dece06c30248e5c21e2e69d5cca4a735ae1312bc61c79798966a2a23002f519fd0c30d70f3dcab50e725c63eeb69eea59b5150eb0f328c72da43e3db800b5c9b490c5ceda5b702f37760bd934718f3e569f7df74e7df2da56d412da75cba1e5f2c32a7014d9521d291501921c33e1b51f62adb0458de4a5609a2dd53274d1a8b51b5937a3a33c3c2b141ecfa5b5dc7658b33bad126d4594b631572bc6723606f015f01918c78c796e7094976ea75eeebaee96379ecca1ac990651f9ab68d903546a6352dd576ceeb121f68a08eca5d588e6c023a53873aef061c948d8b0ee5720af12106b97bc573020e8d0375439b3a71e77f99d2646f9da7297dc79466cef3b1a4ee76d40daaf278f73bdd712c0a5dfa1399bf1e49ca4dcafe0bf6d063c2a223cacd0e20c2788397f965dbc9ca229769fe1d02ae65b19c6d5d6ce9869b7cb4284966381f0e5c7f4182a7ef74e5f9038fe9ae8dafa29d837512c03a41403ec651f566be17ed4a6ce0e751b087d8acd2cba39e1d9377db4d12b0bbfd322c8054d1267d6a29957b306152a3c062e1f0d62abdf64f3bdbc6a39f7255541bda710a3e290491f2bbfc755b913234b835d53d0a5252150bb09ce3d48b5ab59db0d5302cf4485a8e24c988f8afcca247b4ad3150d0542663414acd0be1d4b5cbee1f2482c1b0d4a6f37501e507b0e887710ac201f8f81d877ef84c257c6910ca1222f9227172e555ded80388128f60a613f374ac88779d3bd2c419742a9dc9e91e6de7e7498f1e9b30b8a0b157487b538ce20a6de47050206b0133ad391bf3cd9247033b7c1aa65fdc7da5f6063e941226733bd3c2f2c4465c40fa6d252bb6b7de71887e4e284519690bcafc593b9b8011df50b11eb7145c466e5a71251a836cfa518502a508789bba633e00710ec77df00ff57d048e6161ca5b313eda73094b236173fe8b85301a8be2f1fb70d995f9e25a83ef2da30383fca7bf911cbdab0247daf0d1925757da06ef7d8ff35ae3f8bb7da93d3e12ca56e05bae89ee4a165ca0616da58fc54e597e7126dc1f76263ce3b7499a1d976981e8d925fd53b2fcfa52ac78804617282c69677cebe0128d46f73ddcd7b1998a7e6d6d74b18295fd3fa3edf17f71da0b686f57d3d79dce63bd904e4d56d34d136f88eb958561bba5e6d80b9a5735fefbc3c57ac76e50d67785cd9bd7fb383caf7b33a886607013bb4bef9e4da7b8597a7e8076054fc82560951987766682589a6a507554b287e40af98215080a629d1a8702442beb9f2e1ca4f9a10bcb8594a3e4acd13e5936185c1ab132d44e9a844e104624cdcbe43c57436da73514812dfb2ca1f8260f7c2995c86cdf2aa02df2b4862762554ec19acbd8830bb72bd536ec37d0e674a19450e0ce53459a1e1353637dfdd709203724c72c8978a1c0a505406f941d10392163d5c8f5ea6f1bfa0527805399da0ac8c3aa2a3d05942a599f0fe34f76af7183ced167e944b5042a572db2da86431a112204e3e9760094b2151074ac4044bc23ac19296f93985993e91c35189024be5d0ca737f4181252bc74feaa5594db094d7044b5af7a2f22fb094cb5b82a59425735903986849e3408d424bd4bd5011571e7cb0d7b5fc720a2d893f155a9279ca49599d0b2d514a481f444bf7f2d6e52522ee4dbd1fe20914b91089000c0e2f45e374c3a314421ad98e42ad1ff9f7565bd20072d1d3c1e61503ff909528e5e00dd42b1ac30d736679858bbbc1dc95df6ee8343f2497b990966053114a5d867abbc991a057bd3c98886927b296722e776a42e502f7a4de37714fe65b67671c75159caeff0883dd9a475fe78ecc587251e1d04bc53e37f49751992b2240870a425777569e0ffbf67b77d2e7ef36e4d7b3594503c54d212aa00838eb26cefbf5e4715defc5c2df715d6b5b216b39b0d7a91a5971b1be5f4f1ed7f5de7cffbbad6bd41921b5e94b7173c45d2b983ee13e77ea2a6e64fd0431de157436ccd0f7ebcbc76db9b7afff6edb526194c1818f99b2ae33969b5654e58a8d8333aa82e4cac399701ce5a0aa48a3208b6a1fc295b0a3f459c90239454e251d65a810919db03f41c918d8f39dc82a66cfc5c8290adee3a820efdecbced1c022466d4e34eda31cd40c7194cfb9011d5e778096258e6d441909ca6abe5af6b8b3ef8df4bfdbcea64452151e241386da684a3c3a6a014f1c2cd9c3263683ddabc3bca37dbd23a68372facea0cb33c397e70ab8a91647250cf9a9e0b78994f10fcc75c64acc3b91d0472de61ded3cdf11e3514e9b79e7caf071efdd8bc0bfdbdef3e4b829025642199b634e27e751f34f508e8a8150aae96221ce83657e3504ea44399d2f07d9a8cff3ce95f3cb7385bdcf3244f40952ce94ec19659f9f29f0513b5a35c5980411292197312ccba8edac5ec10c3167bd4d4704679bf9e4a3ab88c71d7b2fa9c7b5231cdcb2db22748f525fd3c6603b420a9037e0dd759dac010e15a99db75cf013566b73a20d8cf454c70d2087016c1d8ee016677f79ce5e12b42d8cf714bb00b5efebe6e31adf3b09fcd3898bf8b4e222b65f3f2ee2b309b45bec46d16e0a8707b65b1d3256d15379dea00ae55f58c66a2fcf8d997729744376cc638a5688b51b018cf26840f2636d90e436e3a61895d1f5064cbb82b75b6cf6c8833147eb4e03f2db8864f85c9b71447913d53d212ab8f82604fbf5e4718dee4ddebf951aa5e4959508e2bdd495553d4880319f3caed19d32e4b752a38abf22f9588c37d3d84b6d62cc7bd940f32ee5bc15d7bdf46b597d35d9af2f1fd7f84e3df15ba931e66cd3a0a312961253ca3da63eca052dd7e7724d314d16abf5f2bca3b0de91940e9206f2ce9561124ca5e0a884914e866a6ac3a9b2f611b57d426d04273f0e98771474be23ae93e9e69d2bc3c77d74a726f970b4c569a149a241d4fd4638c70302f6ebc9e3b2ee4da7bfa2acc23d39658276ba26d38c0518bf825aefe4f75f51566dde7ba543c6d84bfc4d3a2cf354debd554019d7bdcc25593122d9af2f1fd7e54e82fc1575b9156e0c3a2a9114764b324a0ad3456075e0621218d822b0ba93e4a48bbe6eb0e8ebca2e076fd1572592beca513ca9a98e2d4cfa4a21b4ee68af9c61de496a9a471bba96985d77ae0c1fb6fdb33724bcd75313235a3f5575b3d3c136b8820329f4635ab8455b7932ce50b8f61a0a57be190a57ae50b8b642e17285c2d55ea170235e9e95a9653915355f6c93c03ae6b2a2e192b5da925caedfde57f43cbda2e1f28a86ab7085700ba8ea2a5356fe71d3ef05c6df3c0ae53374874b3a29e7bcdab053f82bc5931a7cc363e240ab657b8a2715c130c513655be249f19d144f127fa57822c52e523ca9bd58299e14184af12413299e704a25259eb83f95785247117a696b6052e7645fdaa769bfc493c29c40473901a47852f1a64b3cc947533c594d7bdca5f772ed3fa9809b4f337ce26f2de0e6f31457a763c24e53f5eb9b82efd793c77d7d2f77ffb1af3fd4d756a158380136c25e27bb55cf970bd47cf4b8b3df708eff63677fa0b3a1cfb33d73bd56de5c745f7b5779970a34a6bc71afe55cf70a8f904363c557d7a78f47e35e8effe3687c78344a950a7acc54852aefbd6263c1288d941857b4864cac23859d4785355294a1b5f228d6a6fd5c67e6c1dab9e0d499ad4a5eea6e10b13ab52d0778da2cd460c1185eaa6787e9d99689561142bdc3b06e4f608ae3d2078dda7d6f20e57c9b890a8d0fc6314a8076f4fdb5698fa9e55e39f1476af910b594d3923acd33750cbd740960e2a39c96cc639e196612a3c6519de61dedeb1d311fb5c126ef5c19ce33cc4b9d580943ad63a84b795827a19ad7416a326fcd8336d461ddd3aef9da5369149d71deb9f27c4c00f7ba9e7eedddf6a81352b8f437b297935fef7bf9d703d2dcd3e2cbdd356f5458e57c45657db2b2f03ac44da7459d69c7eef57b3e7e5cb97bd5c63f8c3a79512735c59dbab65bd006ca53b1415ed4590e7849b1d17f5d8a0d78555555602f811d603affea06d4630798b193aa9eb64e4a2852f63e49997a3cbda3476bdceb54b8bd94b9d867689022e3bbf0e66f12f6fb93a3206ae719277a4f02c46ec7d4de96dff53f30f6798ecfbda2e7f73646f0b3d55177653ee3bd8ea52bf399c57e3d79dcce7bf5d1ef713bc52fd31bee33389de0262afbf5e4713bef954ebfbfed8ca961bbcc7660758cc434dbd5b9898a1ba95c663b785a663bdaaf2f1ff7c3bdffcdef6f3f54c8b26220f318706d54e6cbb242f1b2429502aeac50fc34ad508550fa7a675aa16859a1568639656459a16a45c3da987e59a17859a14aab5356285e56a8b23796158a2f2b142d2bd4caf071cfdf7b01fd5602413f17aaab5941b8ab5eb30264bf9e3caed3bd3b0d5e7b1d2a566d120380d2a60447d9e24157fc7c803aacc65a280c00b066b5da8095d771b834c6189aa306b4d13ab444177908becc223ac0c92a9b6294f596559a0b34e13e82a019696dfd7589ba1fc415bd2a3fdc1e9e3c9b8dbbd3ef0182bf8b1eb102a401c051a75012446d0d05269d1185447846a012e13acb18586544ca29087a54a207d4e6ecc8c5d3a139c49157536f15c4136184f4a63dbbcbcbef7bd471790476c05462fadcc0a4318ce7dcd2156c2c61390e6669823bbbcf158e1232cb0cc229345674b13aeabae249959adb67b4fd6ec70c111905a3cb0fa8a289f9d03a88068684344738f21ad05b1151071a465caeb4c74c8494432bd03a5f0fb8e331835b749cea76e80fcf69ffd33ff9c5e7f76ac73f8690fecd42483f572c0164dc047dc7e2ffa41b07edd793c7c371af0afde370fca6c351067ed2cd4377c57960c7c616fbf5e4f170bc11dde38fc3f19b0dc73ce140f10414dc8c60af10768c58db17eb2e5a9c157c424a3b4fba29d97e7df978b8de8830f2c7e1facd862bca54daf9a8048556b875a0449f2919a2629d559b89dae1573b15c570bac6964317491f5ca77557e4c2da396dad028b10eaa0b96348f68adb8482338053ae8fe8f104095aca6b9c236aab6e269a5414139f113a81a5cfb38318e6d9a9c09d6688f894c4ebd41b265b6e69a0fbd5aec7c474af69fe2331fd86c434b7d1191c9560d0dadf54c116a862449b55fc890ab650234d06f30ec67a07554769372afcc2ca3017dd98c1162ac1301d5c66688598aabee1de9fea4e692f4963943e1383e73ba87c9d5fc6e3caf03181bcb18feb8f04f29b1188d5216409d83341d97985738d47ad120832b7d2621ddee9f311e9fc6a30964143e7cb8a36f172deb9727e79aeb0f859c68c8f6fd8ca884be4f9d91350e7438b01759ec154896c2ae0b38c29cf759c6508d90cef2164339f7c7415f19876ee55e17685d92c4e5a6785f73ecf2a702d9d6b99fa8d5ba248b4c1530f7ee00c2dd82a26ab0ef3962d309ffb5923e536afc8ba4ab219c769d80fab2d50d634b88e5d16921578b5af30c7bd880ba33793d8a16b33f323af81d0b0827900d48733feec1559dbc89fa02b0caf1886292ba65456bb18eaa007f19172df8ac85da35811b94b48351ea0da9b029e1c96227465500144f37b441dd54540152fa7b729b111949617c42aa4ef5e5e9ed0e1009ee1a2dbd5b14902732765b9c7526f5a6e0136b25750fce4904d100eee5cfb8489b831f98c2fd6b3cbb942e8d50e90e4591a49372fcfd5aeecc70a5a951d5962a4a7904ad6942b9692d4f9b399a8807ec0dd9b9b8eebebc764f38602fd9fcc712c4feb3896f6ab8e6379ae8027b14b60b9489684eedea6a44e3c6f10253f92e922c98cd3459249a68ba424f3a564b162d345526bb6911f95806459158acf6830c574916483768b4d3f841beeed08ff848e46795a47a3b45fe76894e7daa4569230e08ea5fc4b49d86cbf9e3ceec67b75ff1f6e37cada6ceede7785b5d99c95f6ebc9e36ebcb726fcc176a34ff878499e207b45322dc9b3e27ea5e4a9b424cff27d48c91361bfbe7cdccdf7c68c3fd86ec619df0a8e4a14c8af184d09f24b839d0b26f102f9b59027c89ffaccf54e81fc621109f25786c94f68817ca305f2cbfb3141bee302f9b5ab9c65f88c9317f34e81fcdad25ac65a58207f65f87860efdd7ebbfef20927f24ba7df94f763021716dd2101a1056c8cb05f4f1e9775eff6fbe1b2a8bc5e03e639a211b36090be5f4f1e9775eff6fbe1b2caf5111572f2c0c69d6a972664c34a30c8bb372eb50ded107582a1d47115d7978feb72eff6fbe1badc0abe593f2ac120edc62b8cde15570f665c3d1d6512af30ae33aede7c0555c6ad622d868c2bbb79683190fa41339a760a533ecff15971c652dcc279a7f60b91fabc8341f31d541a54224a458499193e6cfbf7eff5ef78edbef78a369808b66b851fb379fa8ca29f42326fcfdda5000d058673ca59e5d05be6c019f8a30eb2621e907203429d85cc5ae1a69b02d7c9d7467d6467074c9c1566153f246c1e72ec4a47d95b7a850488061051f105a00becb5ab2c09afb673a3c33caa870c9fb07430083b95eac5fb21759a196ab3e473e4b607c8d2ca4c4152f7abed535fa824bbfb8c31e91ccd24a5eadecc7d28cc989a6c549bba28566b00aa957925b4462973624c6fed3aea03d60130d690a70151184ea5c73e08df7f433dff7b713ecf7305572c355d1d1815dacac3b9d4740518148f89b9342b044b4d57d22dd074152a351dd3d354d34dc59db532d5969aaeb6c4979aae5f6aba7ea9e97272602cbe1e295bc754d3cd0041e5a8444b4d57bbe061f94e959aaeee74aa0ddb534dd76911c46ad7e321794345ff4fe718a2da89f9eb1c43f4ac7e416dc41df582da6efbf5e47117bea136ff83ec42bd6076f4bd365f4e4311edd793c75df886b2f80fb10bcbabf21dc446d9675cb084d878416cbb20365c109b60bfbe7cdcc56f85cdfa03ece20a84380d32fde2f471717ab9383d5f9c9e2f4eaf17a7d78bd3eb32c8d476d6e2f47a717abb383d5d9c9e2e4e4f17a7b7cb20839741a65f9c5e2f4e1f17a7d78bd3c7c5e9ed69717abd0c32fd5770fa7b9de91f240114ee9e46944bbef24bbed24bbee24bbeb24bbee24bbed24bbef24bbe9257f9ca2ff9ca2ff90a2ef92a2ef9ca967c15fd92afec92afe292afe092affc57c857dfbfd76882be1eb6a4157b783338cd79cbdfa7f6d2dfd619013e4fbd01d6564601309fdecf08bde297d71d8df5f24c08349d7bd67abd1c1e83a3cf1b57c6b388c2b19af031aa0052a84f28a8b2cfdf15ffd8a1324792ca1b3b2fbf6cccac9fb0d358f93dee88b77656f7b7a91be76904534b80efa8bbce942a157cc5a2d6be7b850eefbe85d3ee5e4ed534e93c71f4a4fef7289f271ce6de385e299f7dc50e8c44163badf0731b84ee58e1a08f0a04ed6d5636e783aef950e1d29e124722f26ea015245a977ee11dbdf33bcb43d1bbae35a5ce1cc989d2ebd4c29cc3cebcb356bc94a31c3009dabb89466ba2e93cde3cbf27a959fab0f7dfd845fe8f78ecd954744cc529d749264b711afbf5e47153ded87efe8fda943a7ca99497b897b8399597b25f4f1e37e58d7de9ff984df129dfbf2a106dafa3a1a702d12f05a25ce8062e0522edd7978f9bfa4604ee7fcca6629d4d5c4a3cb994787829f1f852e2e9a5c4e34b89a797128f2f251e5d4a3c79ba94787229f1e452e25d8720385f4a3cbd94787c29f1f452e2f1a5c4a34b8927bf6291b953acfd5e9d5ff75cbada9a2248bb88ac29d275bf9e3c6edabdbf30787f5d3f697177a579d66c29c3a0025e1657e7a520aa831b5ecf9a8d756633f11936df5a8a8ce52c50f1135e4f5dae7366b3a8d77366cde739b3e68b63f7056d742a35729960a9625fcf999dc1f45ab91214178775ce6ccc7366679e8f3be271f4b9df8b31863a1dcbe0a8444d28a075ce48c89c50a5cbab09d5e7ec9b776afa843ccd09050bc75d19be3c5798b09a50e5f102dcea94d29a50286b42f575e8b8d33a74bceed484c275e8f814167242ad0c1f7636767823d818bf86e545682ab031c4e933ca3a9cfa81e5173bdc0bd0f2eec037b2995f87999f91fdaafceee0145f07a14c8fdcdab25f1e6474945043b28034f638a11b6da12780c75161606bcb5245754f683273f850f97780e2b57ca8f8d3ac7b79dcd47119a5a0f1ab00bcce69b713b8eb51408f12cd948cdc6d5c597ca802773080af58c559de0d9d76d27643e8756afdadfaa037a5131c3602391d0e025907cad699f730ae6f3f54f2dd5266c857d75fd1128ad167d7075c5defdfe87ae83cfb7eda16a214c657e7cf3c6614dcca4dcbf4e1744cddae68abcd90afb9251d566e0c3173ab2d7c99db95c787da73b77ad86b70e33a458175d7be8672ba32d68ea06f0e25c51acad28ae750465f43b9b278792e512d33a30a010a29ddf4995b41ddd7dc9475e6568ae5ccad0254646e571e1f6acedd8a615dbe4118d46112462e1c49186546eb4de144d64918b808431761d0228c52d8272f9db9d00a5fcd48950d97c1a1b251c0cca64edc42695287902eb54265f3b80178a7f1d5eb1841206f6678e4b5449fa01620d3b26130bd4d0c4a980bb021accd344e23ace0acd9a189ae24fa20c6141b0ee468e5e4145cf7ab0ce2d33eb0a70b3bddcd7fbd7c03711e6cb4578859a1b2a541f7323e88f69d3bcda0cdccfb7af74305ddcdf3770511ccd0d2a8ef424b97af1a6e1e3bb1553dcad2101bbe1eddf8f1f7bffce2ebbc7ef6f9f7bfcaeb573fffc1b79ffe53000000ffffee97171c4ebf0000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.ttf","1f8b08000000000000ffacbd077c53551b38fc9c734792cecca66d3a92a624ecb44dd34265cb4601992d588614a4ecbdf72c2d50f69e2aa3807213016951295aa60a41101511c1f1e2a8e24045686ebedf39376993b6beeffffbfe5ff1aee4b927cf79ce739e7d8e8000408d00583076e9d8a933b7981300e60e0380bc2ebd7bf53dfbf8de6880b9150061e55dfaf6ef90f992f22440c1320074a5575f5bdab4c9e67700269703c0b011e3874f7a2feae01d80a9d100cc7323664c3336392cdb0d307b1c00f41b35e9e5f1331f0f3b0a30230f00bdfdf2f0a993400e1a80b9fd0020ece571b347ed5f59120a809a028c59343a6ffc2c8d56ee0598620480c7a3470ecfab1af3f11c80f1c7002063f4e891c343caf8350093770140f2e8f1d36695de1e300e60fcb700cca97113470c6fbbcfbc0e60ec8700e8eef8e1b32631098c02602c79df3861f8f891b1fd5b2a01669703a06993264e9de61d0f790073d3c9f793a68c9cf4caa82797016619015037e0c8774c434e00067850401844c264579852a552b714c29402b8c999a767053d87bb41804ed902b6199c38a14d0e7d009bc109ea36392e16834addd2c54917997491d38b3324ec912b4cfa249c5e04ac7446843d4a49359954264685900a3126e44026a6a1a715aec8107f11cb50d8b7981145843d1e4e78f21a27f3ccc7339faaf02ccf103ca4000f010cfbbc7fa2b9b4076190520f72a136833354dd26c7c9734248936cd3678647392ec46395ba654a2a323376263d232d4acb2759d0e043e1ef5dce1cd7a2c5b84cb493d53d79bd59ebd6839e7906004301abc6669e7095026240606c2e1cc2d04ed804995b609482dc9d928a18b3c6ce9113d2254c44ab51043973b3c41de8970272020c3301d81b9c00064884f512be61368333cc8fafd266702afd0fb4276adf43accde08c25648e0f23d8cf13e26dd14e60aa7b25b06e219e0e57bc5208750ba136a73ce0db50a510e12667b55b902b052d058b720ba014a2e92709a40726c6ac313b4c1a93c3a4b133e4b0ebccf43033268d097bc4c7bd8b7b3f448a4ea288e2fb14f741f23eebfa7cf8a0d3fda7bdd7ed41f24ee263b4431c8576ac422f15a17de2307214897b5689a3b019bd04806082378555f1bba009e44bfd57d80c4e85bfcb4936833389f41227290881154a21cc4dce4a72766a504d8fb0d21913f098a41412dde46c216767a39aaf525251bac56ab138d233321d765d5494ce9c6e3127f13a6d141b15a5d3f2329dd961b138ecda287b5a0693bd78fb27ef9f3db4f9f099d32ba6cc5cb01ca51e7de183b73695dd28ddb062e91634f3c505f6f6d7f7bff6b1f68b3bfaca8fd61c9c3b61d4ec1133f78c29b9aa397b56f5e042d19605001ce47b7fe29672e7211c62c102e9d002e649fd656c0627e3efafd966709ac910334a21c42d84d89c2ab6a64f8cd2191df068560af16e21de269895cec6019fc72b9da9358f2e8799f2a6c3e6ca64cce42ed39692aa49cfb0a745e9745ade9c6471e869571d8e744b12af43e91946fa8d31f00b4220ce8af4f987962e3f7068f1922385d9ddbb0c1cb069007ee610e20f1f12abc8275d070cecd6254794e5e38bf9ecb6a5478e16745976e85091ace7c897fa77eb396a549faaeb4b4b0e1775595a72a888ef357244ffeebdf246f4fba3033ba5033030d25bc9fec155400258c10e2bebe187389bc119e79f02d13683339a3047521c610e5763451cb930498dc945d938895c54d2871a092446ba44a9620821e294cec41abe70991255128f395303b945939e9149b985104ccbcb6428c361d7caf4662bcf9b9392b9b44c2423ccc368f599191912a9464e3f30bc43c5e12d153913c7a28e1d0fcd75df1ddefde2cb9f8822fa74e31c71b7e9d0f6a419333aa6e53dd767185a992f4c9fb1aecbc1b78f2fcfdedca797387fc95eefe17fa676e8f4758f49a8247aee92196b99ef86adeb9b32b0cdb3391301c1cbac093c54fe2825e9c3f9a44f4aaa86c81b8f6e31daccad17d7a1af00c172b131decc8f0515284088b0092c1d7eb53a53cf6346a6d444e965162b5e3efbd7c5960d6714684dffe996e5b32b71afafd13ed4aff39c4962baf84d7f7181f8ed916193bbbf81fa91596b111be3cc5a6d222dc6326b865ae348c7d89a19a556e3cce9bf2db3ee3883b377bf645df67026eef495f8b278b0dbe419e83764337e8cf25142efc9ddc423621e2088c3c399fe9c00116094469eb5199c2c1d79778c00b66841e18e1114b6e89454d4209363ec4c033da791318c1575173f6e8e9a2bb687a046cdc58f2ecc2b3b319ff974d0d649285b7c75c2b6c1e26f4390517c309860de1336b0e9ec29080503080a1bd16a0aa5930f901c0c957c3a93caac32394c2a3bde83b68863cac431684b19137e5c4c471f1e4725a4ad78f12e4a872f400ed1a42d275ba35448bb40086da54246cbafc81fc29e1c21deedbab4e8d0fa91570041039c88c3f12960400d02b63951908832394c385c8c46dfe3c4ade4b7d679ff4433e03a28204ea28ecc6670ca882ac35cc07b99447fe988025bd772ca33cf4c69393ca55dbb94e66dda9036d4dea58c926a470d08600b528f29a97a33b23bf7e2617be6f3cd88969beaad6443b80a0807033c23fd6688cde00cf1cf452ab8c85c0c513a5528584a05754589cd49987128d5f634b5c64ae7874c45054b261b72b7f2877becdd9f7fbacb942e2b5ebb04af2a5ab59cc1e3c53362057220fbdfa83d6a21de102f84fff4e9adbbe26795f76f7e0b088e00e0cfb9b780872c0933ce667072754d10e0c8c4606d4e26609e6386a3fade6456718e0676fc79995884d5f1ecf555872f02825c00d6ce55801e7a415d25ec6fdba90fd43d6e17a3d4939f522a9d8a802f94445fb9747aa5f4832607f24b089d393d990a14844cacbdaa337a3ca97fe1d4c2eda588f9fc834af117710efe62394e59b8b7ff940dbb575f7efc89f353f1533107100cf15632ff70156081b9127ee1368333dc8f5f8ccde08c217d87702ae3c0e6ba0628d7b517d064978ddc0d053445080fe6f9706ad281d26909a093c6422db318a510e716628264654a2ab2676438945247fc9291684fd698ecd39c8e748bd58c56142ddde29d555cea79ffea8f73c7cc5aea057194e82ddbb260c5da5d1b8b9834bc620a8255935fffeef67b435d4d2dc2c28affdc3d35b570f5d285059870a10380edcd09100211d0bd1e8d408504e92f52b0045b6473e2808ef16e0111fbc725e743a9b549a108fa3a339de0c88e4c2a19b3e2ca95324f3e5e7ddeb3189d8f423f6c15df407dc633bf55b5c4571a92b9b3c35bc936e30488873c090b643338911f0b3a04642e20a5930d20a12c1c11a46436676cc0a79c2c967c1aae14546e3212da00846544a9a7a46a34269d89c9b0a7a9755a6c4eb258ed513e9a9a9364b2abddf18f9e379b8e2db8f8e31fb7cf3d521d57ad9bb178e3de65b33ba5e2dbf8d651716a5bf19f7bf745cfad77e62f14766e703a1a125a2ef356b2719c13b4608217a0869debf4029481d2c8250b075f2f74413ce38c0be2089d996765d52883c3a184064c122fd3f9d981e995f49917b4532fbefbcd9f57af8b556820eafbf1d07d89afcc9e5fbc9e73ee661fdf5f263eba795ffc1db5f774411b5009e799346540c713774e6fde524a46e17900b60127000ff67ae63d1d12823fa70c9af0e09bf00899895c651b780ebc8773aa2a99cfb8ae4f4e73d1db808149de4a3682ce7a1334f18fb1ce6670eafcad53b6239c6655e80839ac36d7352bca0d1a5926d64ab82cd21aebb32402e68ca0533a938208e69b32d4040b3032186286519a216a95a665a45b98caa92f4d59ee757fec593c65f8a4ca73e53f6fddf964ebc6a54b36893f8e5fb9fceef242367dfc9194d4b767be73effedb33de4d4d3932eef4679f55ed9fb363dbe3b5c56cecca691357adbabb1a30e479bdcc63dad364180c356e91bf9f1a9bc1a921fd643464ca100303187aa70966d560412f84d69610c43ca27d223ca1d22981333bec19190dd232321d7e8b9b6993b57fd88233a3267e58f0d93fa220be9e6cfdee2ff197dcddc97b66cfd9548c1775ea37ef7ec1861fe789ef883f648803c5d9dc2ef6c19329fdba9ffaf6ccf6cde7bc5e58e1fd138d673b8205d47f019281dafb3733955846d52c1d4bd49bd70b8dc4bba83fdb11aca0f67ec54c453c28c3c00f49d5198504045de17d660edb1d78880ae0b3d83644d4533b0b713ae4403a24c753aaae32697899a702cddd80e6549079b60d1533b7996bc0800cac20c86c02e316189b1382e679a0a090143eb13798db55ef321dc8c134dfef29df47b87e05543077d81e35d850a54db0e1dc0247b0a1b8381883672c33a0ea082e44ccbbe28a0de2f272c0d0c3fb273383ab80683041df7a349ad166701a89463304f8c460202acbc52ae1dfb51a485a0d65646466566bb50c4919c864bcaec6971e89d39e5fda6f42fea879a5f89b53efdc78657cb7733ed77ac7ccc27e2b878ecf9f396ef02b173f701e3f32ee856de2fb35bef618f119fe16b7151cd001c6819026592d729bc129a77e92db159926273846da5c9c3c92dcc995ce963860dab98534a5d310f0895c2934760b8d6d4e73c087914a673b1cc4bed89c444d7c4b66869af44acff0c46651832989c5325ecd92273d7502b046ad4eb6a7b16ac453bb265343a8c15f09fbece419474651f7b51b3491b3cfe6bfb0a877ba66e3e4d5bc462c155d97c54b2742c3d621ebf541275b3568f351fe1371db9b616177d1bc87ffa0bc379efe1ef14276ab89c9a8798b76d3b7a3bf7e17bf3fdcbfeff71f1d40cca6266d3db71edc3b8196a36d17c582bf1e8b1bcf35354fb5a6dd4787900169d05b0fef8983c4756bb78d1aaa407fc7ff42281907c0c57302c82004fab94242c308ad429402ebae6d6ed78e35f0368393271241ce624ae020567685b0bef8898931233b91b10cf39167cfaa0bb8e961dcecbce779f4f0319a272ee38427bd712c2e01064a00d8026aeb47412264d7a351ab6d383d5015aab73923037e531e49cd2d503a95018c896a8920bbca546399f066a432a5653894a6062649939a4ad09d9f1f4ecb9bb14afc5ebc885aafd8297e2d96a3a4055b57178bdf724245f9a8dd4d4ca58b2aeee112cfa3a23948b663c1b859e301c14c6f25fb19570171d0a51e095a6d19872a854837118cea60b11913f428c4118f8367cd465039d2091f81de6cb19893b04ea7555333f9338378ff1751142faf4721c7bf47d1fa733187b69cbe7ec1b5ef681cbafae0299a8232567f80d20f889eefded825fefa74cdcfe2f7eb4f00a694bec009100a3abf155f2f9d09edc202d002a533bc362959b55ea7c5ac8c51d9d3d40e473ab696a09defa2e4bd68aff879c58d2b77fffee116271c16af5ccebd2a5e398839f5d322a4f5f67f8c3498c831824b678a8bcffaa08c55c7920b657932b6a1c1029355061b764aa73c587eaa88cf460fe69e6715feb26a04b3d1d30ccfc4fb3d55bb39618fd8d48f838d1340011dfe0bdbbb14985a938a3a38f0b57ed4ecfbc90f3dbbcb99159e2c3c022ff5cc273f170108e6782bd93b5c052412d9f57fc027ffee47b912241d9c603b7136e15a02ce3d119990482e6d137a25e05c677c205e0d78960825ab4309a634566fc6a624ccf878892819f64e77f16fc125566ec5c88ea2be455189e519e2dd7317d1bdb3e35e7188c7b1f2ec98fc4328fd8385a81b1af3c32d64127f13bd33ff14bf4a6989baec92e8c8c9e958be5ccf5806398ad52c1638ca0250974361733d54a0dc13d7145f29f06457b18278290ae2af28d094c001a7ee1a21b75d6552d939f9394f527939be7b0eeff70ce504cf5a3c858cee5b006826f570e3ff4b689ab6654733cbcb3981bce5f056321f72024482ad1eab98aa7e82b1c24d0ccbb0602f4825996f92cb93c9849e7b7afdf3dfcb572d99b91171c2d37fae57debd347ff596953e8a719462d3eab15da93b5faff4f5c7ac5d808919db4ed51d06c118980385b0038e4019c8735d2d8963c773440eb7d364f09df901fc287e06bf82dfc21fe44ff18adc945424a3c4437684ccd8e02a2fc72bbff7bc8fc7fc071f2815b59ce049c16ecffcaa1bbe3912c709c041a3ff32470267464a6a03320dec680c7655ad39b77b372748edf07da894cfa987ae414353c31681dabd3a1e4e1d3c60692241ba8480e4c829b05d655669901969546615da8feea03bff94abc5a48da25953ce094f5f645f7bd21b67e2bca723d96d9ebd9ecbd298fbf95701cfb95008995af50c0a45a45e36f64f5d17e6a4f406bd10c64284cc486251f3b973f8abb3ec4b4ff770c2d3edeccb806186b792537015a08244bf3c0ef2dfab0911ae0cf2b6d8dade9616fbe678ba6479e8ac34ce22a3d35c9dc929d6898f8563e25f1bf106147aec380a5d77d67de6d475e64669d9870c2eb9259e3b5c825a5ecdff1875385a229efd042306e9c49ffe1afb54bc8f223de0d31de5d4f7d6402f578856e7b318c2dcf584aefdce972b9441f5886f264809a6a4123299d2589d9635fbd4b1d584f2cb71cc431429fefdb758827276bcf65ab1b80b67792e70c2a3cb37bfdbbd7ed5925d0c6018ef0de178ae02749004bd5c7273b264f509313ebc5436835395508b995c8c9c867755c1ea57ae741a6bd155c6c91af0ff465ba33a93432b662376a9f851d7ded5f41550e8baf28fde3d799db98ea611fade10af0df870f62375358d8f88e73e4108213da1f113f19e8fc68701d8cb748ee861848b8ba6411b4e2944fd4fabccefadbb949255a60cb6ca42e904217c1355576511c2f332a433233fedf159f1c839f4f55f48b171291a70d53301598b4bf66f136fe3e73d6f72c2bd5b0557d23c5bc2f08f9be6ad5c87c80ceaedad64a7d038549e0bac0dab67509022f0a7a9049e66a178a5332200474b2cd5f49660bf3296a66e626b7bcbc4954cb6a7d5c9d79069e017c0e9163ce0effb487367cb4f8bcf1ddebe76ef5634f6a31162e5832d62d59a73efbfbaed952d7855976bdb8edd9ff6e1ec655be74fcc993b6aee6b139d9f4cbdb470d9f679b7a603825400762bb5936df574a87a087865b069e0f6d3d66162b78a4dcf8acdd8519cfac92f9c7a0fa1d66a6f257b8413400dad5d9c465b4dad20911814d9f2eb9d408a9179c3834c960456955d8aa13a54ec11247aefd81f8adfe277dfd8ffeaeb9c509574557ca2c4087fcddcabb2ec39fec61ee60b40c403603d347ad2c2053279350e4106619000ac167380182adf1a5003df8e8bd0be8f3ddf1ef17c77ddcb3a9ff496646a6b00fe67dac79c7a044490300d9aa2d45af0b375a4cde08ca4f29e09219295972ee1f492928af499c88ef4991a828755469041dfc43542c657504cc3c4afcf897b8f8b37f57af1f271715f39faa0fc75e671955ca860be7dd29b6d3071e2d32fa8666a09c05ea5ba786c3d2408b25e8234933fe8ee024466573bc50ad80207e114b0b92edef7d12a7e3b5fc297f26cae8b01c2df2e050fbef81351bef43f3b5e8672bf16d3d1a7df8a3bc46ddfa0cf44fb7d66096eee69ee49c22d3c97f05dfc31a16924007b8613400ea9f5601a6435541bd01272d2ef11a568c723d1bcdb6248b91872077f82bfa89ae8b98f1399f5a4fd0100ec1caa073bc3bf1b22419285052a6f644a2717f0a98293491c62d2991cc8e430e9504ba6fdd33b4c42d543e6efddbbd7b14bf6ac21bfb841bc8443f94520836410589b20ab636333410c6f56d989858d43cf9e1557a2d9dcf7ffccda263b4d5a6ae85dcab4f0e733b02d683ea5a4120e6928e0610227fc43ac1a8578092da5bf6ba5d0c826e0a02e101728c8bcd7985426875d6556a039e28a73e7f8458fbb6ee30bc82fb7c39f335a3a93ac4133a986e1dd2e2463a480b4c049d50074ea1c79f7186ab210353eca9e1113f0a79e468080f5e633db0080f1e776fcd32ed8c4e24c0e13b3ad6a1cb369eb56c2c3ec6d749b276f99414036572442b9d5a932a494225527227122c6c4fc233f7f7baf287ec5de4616a05ab492f991ed0909d010e6b9f48d1a53375b29187d9a476f3338f5b1f5cd826adf1ceba92d164a2f42a84dd02b83bc7475a8dea78312830385e620c3de629542a0c9767b757284c691f42aad5e47e38566a34e1bc53454862c7e73df4708fd7062dae4112bcaa69e9f71e6266b114307ee32af178f4d33f65971b2a8e44cffe153f3babcb035fbcc6b62c4e66ce59a415def5e1cf812e93391c2461e20026260944b1d6b20e8a99502e306416eabb137636b7b684161104ece5013348c914a636abbf7fa5a068539c92a23b6778651ada2f92f998a486ed6f86df9479314474a27a1c2fba51b56bdd5bbff89e59bb0eab17863fd7c1e3cef1789b7440ff7ced52362d3235701c18bde4aa692ed09f1d0c3a54c48ace63a1ace8bad65ef044d2a39e9a2a0a4851eca5a0812fada23b03909540e1fd969f481d7e9b451b8cd884df2e3dc8cf313be149f4efb7cd3a9dfe4c7e5c5f96b76ee583a6b50eee13c644590b8e7af82db6fe4affca0dc7ce60aa1f2506f25f32b0f1009b130ca156d8823648a96a8ccdb82a2ad41267eb0bdc6316194caf442a8acf92f49464dbada9ea657d95566c24b582a5cc854916e0cad3cfbeec4d2238a8917defeb974db0ae185bec70ab661cb3fc8b604a73f81690528fdb1ecb47b0ffa6dfbc704fb5c6f25f307db1374100fe35c724a674240bf4546118cad3d1562e534521a6b3b7136f65a2ccead1d4c510626d62458792dd38c4b024775b4dc44f33d997a1e25f13293c362c1cfdf112be77eb9f8c68f1e33fb66e14b05f6c905e2e793b6a97182bc408b4cbf27edf7148b3f8a9ee7f79defdd21fb3a73e5d58d116b761269d50a005fe175a08391aec8287d35dfd09e1803593d36503dd7ee23213a0a369dc383a4a7a0710b5a774aaa5d6576d8692e432f9318c9ae33ab1e1c3d5a76a45ddb109b63d04b0f1e30478b27bef1ae6a8b22ffa529c555fd01c3307100f33bdb1362201956b8921a58088d9288d9087511a4733136d043d6fa1ea8d892f8474f73317a1a37090eaeb874f44b22afe283b92aa92e5765fa9c7c6ba6540c43182cb386bf86fd78eec24445c93f1f4fbff7ccb099c7566e9d58feee4f655b561eefd3ffc8ca6dd8e2414d56cf7a7aefe3dff3064edcb8ad3077114afbe3adeb7bd12f3b3f26762100f3150fa082eeae10b5868e8dd2161ce50f8a429087089bc119511312207a2c2448e664d88dc41db648b976e290769a7b1e0d644a51fec4410596d252e6adade27c8f037f387dd2b09e551e1e08e713f53c9aaba0d5736d5d7c7804699b570ac80d7511aae60aec2f55e2b9e0684d40315dffd2d24b52c09fcd4289cddab419d48a30a5f7acd803e5701510065144a6e9a3a56c83006ef81761ac6d931318f813c29482ce2de86c4e251764252b4d495687641f3334ab46f1e8217ed73add98dea6bd26d391d1a2c5b80c36eb6937f1927a93fcd9beec1994d8b4ad841922b460c3788030981ee08819038554ec7f8fd6c86934469705b5e335572024d7c5fbbeae1bacb9c087d4846b322593b16c5a69e908f4c27d7130baf539fa73b6b88c87aaa133519ed8ca5308087601a0fed416088c7b19eb8f7bf52f2d25e38da008809f45f5c92c171ba04f82665a5d71175bababf4416b3338b5fe0783cde034501b5e4eb3ee1cbd10811118ad8f2666919e48890c1fbfa6fbf9359de848b30a657133af7cd327e3f519288f2f1d33ffe582d0b207a79e2d65b366ad7ea3e73071a5a709be326deadcd19e347cbe7267d58f6c16f8e714db1354d0cf15ea9b5375fa15d415bf6aafed8150f794513a43836697bef6f442cff0f32e054c2f36ab6057e0f462b300c3606f259bcef6844830402757585cbcafae5613e8e9d75689414a8f0d0a5dd0cab40c7b1ae8745a9e372781bf5a8f5271f0fcaf8abf44aad9f737dc111f961d5abde660c9ea5587b175af58285e15c3f73c5d8dd2aa14276e7f79d1f5e56d621989c3d8048a5d2c4c7269249dada989f504cd44eaa706d14e5bdb61e4c26ad948ff4d7b4b361261783dd57e814652c2d7e7ce4f2e2d514cbefcfe37a5bb0a0ef7eb7b74f96eacfa5bfc78bee76feef6acd5e26df109fbd6cdcd9ea79b6e50eb431cc63cf4f5647c2debe3bfeb11caeadada71c7203be4ffc008c9b4abccaa3a46c88fe7cb27ca8f944e2aabf8a174f7f20303b35f59b1075bbca8d1e2f14f2c989d8552abe46537376075f1271207e3ad6c4f08872e2e3622b29a83e994ab776656eb87d0a03c894b11eaab7db048538a52588fb7364e3dd5bef40433f850461cb349b6d3036cd6fcf51164f610ebed0cdb93c6751401719da041f71762ba98387f616e38ada45306d327308419a7144c6e214ee94cae558c9b5c5d5e59ab1297d60f48952f0c9e2c3e38d0ebfef1d3df952d7e69e494314877aceff7a54b2e4d2ee58aa6e42f44893dfab6ea37adf7f2d3ef6e7a6e427697673bb619307be0fae32f1e18963bbe3fe997c25b890770ed4007c35d1101561075a863038b1f62eb56f548f57cc44364824b60e4c1a93c955b5013b1a622160f1d7e1df56d8845a4422baf5cc9686f6cd1add3bc05e7cf73edc427c59ee1eddb876dd16e29c47b8b110f080abc95cc1d360b94d0cf15a2520747d58210ab57fd5057402affc2f559060da821a3b2abb48433ed2a33d370dcf5d74fa2d24b373b970a63175e3e8fcb3d9dfedac3689e5e00042600a68ccdaa3f6214c47ab53d261770411123f41b0a7b479cb7469cf5ce53a6edd30b92a46e02c0dd60b3200c86b9386a6fd4d37e109f07cd80fadd3308a5a287932e21a13e3c68c488a0a2d1d8d9eeee52f1fc16f11f2f6c112f9cfea46ab39779e6e90526bdea4336abea16d31810340260aeb05910eaaf9a08428ac688eaf58e6b624421d43be6a54bed589086287686312364477ffffa99b8078dbff5e4c9a768bcb8e7163e8e8a3c3f78be449bc5b1d88cf580204aecc1086c1644427717d03519f52045edc2a061d1123c22880808aae000325d2524b0d96a75e8d3323289a9f14bd505f1f99177cc9dd286e4273512175c41914c93a789e2ef4cf816f6b99113d8e664d4b2019837d9ac5a31a3dafcf8ff2d66948567571dc0fd3d2798f4ad5b0b9898ed4b69bc45dc80f7f0ad211ad25c9a9858dafb709b00b69ada3457a20ce53a7926c0cde2653ed1677138d2cd8e3487238378542a9d5616a5d3a9f57896f3cd9b37f1f1372dd70f1d62ae8a1bd2be7bfbe21feb7f77bf75df3ef559f13f57bec919f4dd55f1e70e80e0b8f83beaf8afb5b388d8551d376f167fe7df25f86ac40d788a0f5fce876fa84d88b6b954a1d4c655d95c892a94eb5407e0cba955145f7d86c6919e69713894d6cca828bd4e6bd725111d2fd37c203b79f2bac5e942376f9e3cfeb43d0ab9f3a05f9f1f3e40b1cf4e4dbb5ffaf1c3f57f5c79fbeb3440b0146d601f3249a0878e2e15cd29408d69134de35342248d0668836a6385c83a122dd44d2bd5cc49cd3111cd322aa8a3f4fab40c07fbf0ccaaae8b4aba37eed96fffe9a2e7560bede29f1bca28f67ceed8943c7608de77b5ddfe103c6d28602812b7a151ecf3d4b3b0077816c4cef705eefc133b78690ee6294934768d59432c78e2511c395cbd32071f7b5ac93de7f72710e47b43997f380b98a09f0b92ccd5fda6d6696ced70b3cc1d142409a141122d15e25aba0e46ab741aea844a786981087638d2d5d20292c07849f7bc4df2e3fc8575a56f5ebae22cdd52ce4be1929d4b67e72cb81cfbc38f4928ee8ef9268a377dfd95fee6167fd004c1600096f85e2658e88aab8d79ed94802b514bfdbe449b6b68229ae22a4e44935d6713516e507f82ab2005995b90bba5992f24d28283fa3b89f45a99496672d83332321d56b3c3ce04e9668d4987cec9dbe02fd1d3c7cf374cc3f203fb147f5e7af7eed98513f39728505b3ce1ce8ec1bb772b960e937fb91bb1177fb9e91c3b6fc77cb16a3760c8f77ec70d667f03033486f62edca4a9a4a3046ba087498646e9265e300e0e1c2a954e6b6d65669559332d8ef44c6b26356c32f5b2289d56a697216d54541a2db6a695586cafbdd7afed5db1a468f4a48d4b36ee79bf62efe6655b278c2a5e56356c6ac5d71593279f9f34b962eaa4654b0a56aed975e5eaab5b0bb6cc9abd6dd58e573fa8d8bbae00cf9d7b73cedc1b73e7dc983deba63f2a9fcc091005935d40bdd67a82f335ae6028a23962142a892a5f39208783ca0115018fe16e41468788a3dcc8d0d00a22aeae14d537e9e83f29dd44fe213b36a0f44fc4f58813df461dc5b7cf89efa136e27b4826aeff8c89c15b3d29f30fcc13df451de61d988fddc44ede28fe8e9ea7124d06d1d532cd9fec727188699292aa51d955c46adc58565626fecee8ab7e602ee1bf00c32051c5cee62a201d3ac23297b95367d235b35270d44452e96226d254949cae3b8a523adb0696e147b5f545d622dcc4350c0ccfa498694d5f8ad29919f0697c668aef57ac6ebae829d88790f9b28116abc591595d82de1c13612ae50ed91a03338a4dc00d2c565ebad7534e61679775ea2a567d3be972e7a2b213bb0a0eaebd7eaa3cf754872e28f4eef7882d3d5cb4767f8b9528b1647296e7fea01ebd7b642c47714d7bf62b42c7dfe99f5a9077f8628b96933fc24bb64e1f9693df2665e2ce8965d9a90579072f7dfdd1fc9d535fead4bb7defaec3171f8d36684676eadcbb431f957664a7ecf1642c46b2bfe25fb8f3200715248020b305aeea10422937d029419d3e2998d1c07f33d2574af98674e506376bdd7a50ab564d7c629181c1de9fb8ff701574d6b584ed2e6bd633b48099ca3bab52c8f0cdbee63683b3b99f7b336c066706b5f0339a4b65c02722994406e7ba429a67d04200db09085186e05c57045dfde854e160d7a8e6d165a0d93b2143e934062c546bae745ad9e08268622b07fb02beeaf1065a356b4c563bd271b23989c5b27475b29155eb7c5510ecd0d7f7ec7ee7ecce7dc7360c1d3c64dcb8212fe67ad03ea441ed907adf1ef1e7fdfbc4cabd53cfa01e6811ea7ee68478f2c279f1e429dc7ffbfe1ba72608375edb3ca27fe1dcc9b30afb8d5c764cfce2e041d4e0d8119474e88078e7c81534e8e245f1e095f3e2e1ebd750369d39f84d2692d2b3014c744558aca46f114a21de1d5c36ee622d3a69fd89006e1766753459422fc41ad0299d1101ea5f15aaf3254be282166a06928c1833be644986ddee482704b24ae45269f552a684b0341e13c63fb768e69243c7c6cdebd46bffb155cb77c48aaf35c98e9bd437079f4bb20c98f7f2c459e92b33ec612f2f295e21560c7f616e43c35ad4d63e04bc5e18055b993ce64db0c0ce2722c86027eae81501c14234937997310207069ab9436e01d96a15e6515bc8e43031ef9679e6326dd14c24df0fc0c220ef4fdc775c05444002a4436b78dd95d1a6adc41384bf33a884cb500ae6fa6a4baa97ed6a8257431a0296391aa99d2f186dae24a9522ec9760292944938d7e53026f956409e75a0dc13e0503a70ae3331803f1b263ae8cb4ac1e6166c362149e96c8d833dfb4076cbd4aa597b5a0d3772b4a682281e62aee975664bbad562b5daa5c53ee641b5196fea3ea4d9b71f6977ef162b296ba2f23548bbbd64d11ca45a1fc128a70abbba66f7cc2bc49fd7e2be8b843b4b50d2b1a3c844b8f34ff10de685a5f3e7b64fdbd17a8045995caa1ac62ec21d32bab5265a6a2d17c534e70e0003312ec472544b619b1394b5eace99e69e61781f17550c189630e5781e5701a110e5cf38570f45985b08b305e92946e95407d3c9b716c89c64d1fb4351daa8259bc78cdeb87174fe166f46f7ee19993d7ab0e5a377edcc1fb3654b549796593d7a8deb0e188603307fb27f4028444257696d5ea00fae50d6bb568ff8e08125255c04b1184f8472068ea63d0316f2d13575abd03871d32171215a7c08b5af7a87799619b0557c09edd98a763f0ddf46a8d601efc447b9331005035cf200dd4e9daba062b0a83639cea8006a60b78b8988928481531ef045045d82a48d8af0d5493b50cd2ab00c63944e1b894df8a8a704351ddfae558f2ebb0f21f98eb1fd04548c778e43b1a33aa5b67eaee5944d0b278c1edb6b3d206883d7e11ddc19b0c06a973120421384215d081645ac8f98086921d8a989b0108a81c975b50534f9542424828d3cd10561114aa72c60428444c4f8eaab2d01734c4b5784b9e2a51663944e6350c57c75f031704d982eb040d2916e352345bfee5ddb766ed963f7abf356edeadc61f391826507f7f47ab673b75dd9eca8d629cdb2ec8d5e9a3b7b74e6e09846c563e62f18dbac552bfb640c08e6b2b7719c3fe38d6dae484c33de02d84e90cee05c624a32ee1881a18b4489bfdfea9ef868330fe26dba864b1cc6fcc266811a5e7085d7aec1890d74df6303c329da5ae1a0a0155ea1b2706950555151be900a11d056955985faa297a75d6c537648917b64e30ba56c9667e5abbbb631c6a717a6aeec2636e1284ecf0230ed688d5d1317a635f950b75aa63acb88402e9519aacc49c9c4a431a9f08a7eff11ff4621ff41e1582edefac4fba4376977231e8b6633dd40017a17e3ab6fc436414e30ffcc7027c76748d0a5a21ba5a5a2f8f39476ed526c6ddb92f7a789cfa3230010011d5c5ca4329856ea5aa1df131adecce35c17e22567dcff23c4640d77a7a49a2436b04a1659268a0fdbbeaf58d66b60bb978ce9860dc3278f4db7356dac58417e770ede891ed2ca8af62e141071aabbc0332a78816700f3fa177822b34ae66860470f777f8646a8f14e9cd372d174c03007bb182bcd3ac5420717234588196261050b3c578881912c1da79209cef03375855e3d2be66573dedeb1f3ed33e287efbe3abc5fdfa1c3fbbe300cb323f6bc7fee60f7bdefbd7780cf9b327564cf1153260c97b4ee36661673122cb02b1c4006bbd018209ff707e0d673af83150e7abfc0dbc00abb5134c8910c4af02da85ebfe427105de9d41b80bb4cdfd90d9520a7efdec2db900c0e300935eff8ebcde83b1301b89fb933b4fdadf49d12260d00c9e030a4e29dfe774c3683d344df010c1dc585cc2cf679888624c87129a44a4c85528873576731297c2c91434a138d83fdcf2545824919b8162925154530329944573b2f593f9999190e1a1070102ec6cfa146c34e87e269e3f3e697e2fb6f55dcda37be836f85113e3c66e18c73231a8c9f3a79f0fe0f2fbff9faf1c93d37a0d6cddab469d6b4552bc0dedf01640369f5b7dabfa236a8e2afdef275974a627595edbfac4a7085827f2543ad6a80408da5f645044d26c6c49889c2d2d0039918d9c0aa35919e0f3ffa9a99c4bce5795e8b7b7a4e47e1734f8ea34b8968b8b89713b6898db67a7e46b3f097802007ae325fb22320145ad07d011452f24561738528682c9465e8f60788462409c7736e978c0df10d5aa83b2555835026427a846408e5a0eee2e999a81bea36533c8dbacf144f8a275137d41bf59a2b3ae94914e6a217c4e34422cff0becac57395a007233482d12eb67113c99a15e2dde46c71d7b3f4ac9a9ab14a1a2335d30b71f00cb44e253eb82a3499b414b844d95f066d9599332dbeb533fa4cbb2c4ac369f5c8624ec2322edd8aa26a0aa28f2e1dbe76ddbb1b50e8b15b9b5e5eb37715ae6afa124e1f75f0d47586d1b5f07c3178767575749f9d0fcad1e8be1fa30e47e71ff09e116f8f78b486299828fef457f8a9a555c5ed50a407301c0460733901c2218acc002983ada8912541f59dd5110104a1526d984b29dd296d41d301296b577d9ad224ef9658331a95af6a170d43fa8d77668a8fbf117f292b43bab5fbf6157082f845fe7b0bde7b205ee404f159cfddadb3e66e4580a0c85bc98ee1d5d018c6ba4c34f21220388cb5167f06ab9d100d11a8ae084ee35b061ab8e6d5287d6996bee494ce86b5566df86d019d3601ebfc95149235409c73c96650a18ea5d7ca4e9c5fd4e3ed011f9c7ae1a4a549ea4ac7d8c9dddf1958343afb363bd0fdd39b7be77fd0c93e745dc1737b5d4de3761a9b0ee9933e6c63e10b39d7fa0d192b7e010cf4f456f2f3d89ed0181cd01aaeb8b0e46160a5d0dc4dce2debab6c8bb7199cf1758a1a82a26d389e9a3d611cad1909b39d381b762d0ce70afa5a653d61b4e0245682cea21721cb76e26cd6b52c9ceb34058b8006016f3694609bb88530a5d3162c1633dc29a99cd1ca07787d961ab74f4fcb857452b95066a69eaff101a3a4caa1740bfb8bf8b4a0fdd76b0ebdfdcd975f4d189dbffaf4ef67a60a696ddfc8fff87b4f43d91b9b37cdb6b5db53a5e8ba27eddbe9abc730bda76e56e1d815da130386bcbe75f71bfdc7ce9df082a6f864dfbebd068ade1fa60a679f4b2c98bda57bc6cf7842dfe77399f4a34589cb3612bf7881b7926dc63785786802035cb2a6cda490bfd0d047776a921a6ba749226364bef59b2a7a17588223c86ab11352aba46cb77f61b1de422380d6cc287da69eb15a7ce11dc25d4cfa3fd33f5bb1f2ceec49d716bf3a3b65cded6ee5cbde6af9f0f83bdd466353d18beb5f39b468fe764e273e165fcedde359b7f8de8a8d3f2c99f2fe9a4d23960e4edd9b51b438afea8f16adbb9d3db4bae2ebd344c67581636c33b60c1808073df475c9a46a7d19f54c644a41e5aebd9107e1bab06a6b880fb48b94d50f51353619715150b55da6aabe639b55c532fff1ec930c359fb9967d1a65518bad4d9b2a916ef3d1b62d8df6cc61d3d9d3b47ecd02235d2cf510880c3650496caeafdec05ff3105899e9329959491d0bd154f20684895d318648dfe61046b7bf12818a5e151d1e2a91359210e67d3238c33178eefde20943bacd1bf5febc7beb460fe83e2fefa3d2e168609bae8587f1a881e2b1cc0e4587b075afa7a8f1a68f7688e57bc455e6cd1f6c4699ef4dc4a78cb74f9e9ae8e91df3452960d82c0e63adf5541886fd4b1de7ffaf350aaa3a350a565aa3503af9bd7a4a1464fb3c579605d72820d80980fab359b5ea756a4b217fbd4e59199b45e6d768ef4f32055db96f862d2e94dc80ea10a510ebae67ebb0baee6aedb54bc4e70b925f40dd3a8d2d683506045529d65ef41fa60c5e79683291b989adaa74356188ea25999caf0e42a6d246c91455826bca2be26a5424be32ef08d3d9d30ccff4acc25f3e5d3d79d4f871afbf87dfd76d41914791e318526dd5ee119beef6541922d8b591df5d132fdef8091818eead9401db138cd014d261bb8b7764f8324a163739a7d4c7e341295b7f99608d0e201c210ba1223c955e84545ba01626e23ea0d0d7a59560632558c9360988480bcdaae78524a5a3fc525a4f5560dd929d063e793d7cf6176b575d6eddbe62d2b51f3d69b2d756bc3dabfbaa3f577ed4b6cdc51577c4c7a5fb8a56eddb5fb8f215b6d9882d51386215b6ee13574dcf1b3555fc69da91f3e3162c9b943762324a11156f7df6e9d5539fdfbaf56a61ecfc2d80a084fd96e1790039a44b95f76e41663b11294b94e15c17c712e11b2370b66801b96304648b26d202bb5d0cebcba3eacc5699d9a1b133fcf62d9bb7ff3589fd25e4c89130a424bc399b3d81ad7c6f5080161abb185d94cfc20c77135354fa29575b19ca953e24e252edcf2cd2ac1ee75bc0634ee2b175dee017e7ce3d3d573cf96c6a4ac76753d33a708b064e9f716cce9c01b68e1d5eead4196834e073a60df73de8a11dfd09b95b90db4e44ca13e538574a5c0aa13641ae744606fb6e5a2630dd29e883f39b54ad2660aa3fdacc7db151abe14de21a65cd9b9d6b6e31cc9ed8a4257767dc0a6d9bf0b6a9b2b1abb559ea8ecdbc5e781517b16b9824e80cfb3e87e73e6a47bcb6f5b888bb2c7db60c468d6a47301ec6a6a259bc06e46072c91521c13eb591082405b503e8960e7a9a664ccf4011637299d3c3794dd7851b0ba68dba040852d828d4858e63703bfe124517487be1046c40c5f3dbf37399b7d07036aaeb9275eb96e45d0404c9e277e81938097248ac6ee77f6c63b5267f30533a42fcaedbc20d33f2465e213d1dcda6e233bc062cb0514efcd68de859eab7b662a3f00a1ec0029bc2c8e79b505b5a659a2cdec079f017cd75a4fa6a5e8510e28b80a0aee7d7a565c28417c3dd0198e88351fa327f3053365cbcd175d5f6d2d7c6dd6847af63af038242acc749cc074467bb427cb5548c4d08098a84046c865738f1c5c153260cc99dc8b003278dcf6d3070c2c46cd29f4dde3fd9ab701d2ca06601aca046a540faa566b26935e538d88e8dcc49e089a7ccd95c58c6f9776543c88c1cc88e12105e237e8eac6bb0459c8a9669d072eab56dc311fef7c0e6626420e55b8837a64366841e20abf8f95a8445913929ced58873d15a540c089e8109cc182e091850d0fc1df9a94c2bd2bf3a1a5f1c3d816eaae7f54206de89cf700223831200bc93d62f04ae352354f5d57db1715c0598a1c8a5a3ba45028cb2199c517536c1312be80c37db9c1010e85404ed6a2200ddaed14c339966baaa2fd1762a323131d196c8e406060f0525cd496bdc4254ed6c748dcc04c6ecb03bcc0ebb1afc95e5329a06655e1017b1270e4c1ba59d35ebb1f80def72b95c2128fe8f312bb5e327bce6e4d14271115a84e3dfff38695f026a80c2e6ad9a8f789498f88af983b7e7af0204877111f3139354b30287eae3e83639757765637eaa7a877916176d25bc9c08c07c442beb6360b94b2ead279113a1f2ffaac29eee6458af0f4289ff7f5f7b4fec882b478f5e7b67d2d1c4f99367e67ff925ee5656c61c2d7ef1e495d6bbd2c68e1d525c45ec11ba5680f981d741140c91baa0b6199c6a637de5927e5e0846545d679f06190a4a65f24a21c22d15244ac89929ca4445722a339332efb94b0f1e941e3d8aca9d235193526e84f6e3b1c555fd99a3c5335e3fddfa09ddc9aa15009fc6ebc00a4d606f009b06d567fb1fe806377ea4697782eab6fc0f8d6d066763ff43239bc1d9c8dfb7ea5d39c983d566705ac9835119e4d235700bc85d7b5c0cf58e88a9cef89855263a46816b24d03a69b8a6ccccbf7bb7549cc46e282d0d5c35210ddd9871432871e8f049e34729d3d0bfe3441029fcc138fad0d0667036243d31052d7b2026147213ab5fed16d83a5da22b6184864ad2e158b7102eed57565f277d23aa0a1868932ab887be3196865c9c14d83d69b06b46bfaa3f20e800808f32c6fa7333d1b5269053179c9bd1ea2268003242fb6f291adf167dff67b999fa32337300d04346073c64040457fc88552f91673954376c2d603735f80343d634600d187a7bffc4bb1923f53bbbba18c9ef0c885853d336bab67d1fa2742a8393753a5c27dd6e95d2ed7a6bba3949a6d3dad332f1ee6d6b8a76a19d456bb6e1dd85cbd9a2652ceaf4ca1bc7f6ad7df58d63bb65e527de7cef242068e3fd13ef608c60f1ef6711340835fb10d264d13c016cd1a716c13ad8074c6e9d6c13c8fcd9265c3bdb542bcde48ab7187d3bf8fd5f679bd007ff9e6dc2900180cf30ba3a3b0f46d70e5e06ec3c1840ed9a9d0743f9da3b0feacc0e9315d9915d25c3bdc78edd2daee2d1ecfde2b2af54a8c9929292519817a390298ecce7c1de3ff121460786a07d07a3eb56283b59450d916af61d8c09201d27a37c53bdefa026a800890a2bbaefa0ac26896bb5d76c3b38290ddd155f8d6adf7d51f1be15b30ff13b4316b7efdab37bb767d052b461d991f855b3e7ad5f3072448f8e6d9e6f02180678ffc4479904d04062d06e83d17556fd299d28004b3e8c0e3b6f736a7190a36908625e9d990dc0d3e148cfc8acd96c30dd821baac447faae5366acdcbbace8d0dd8f16d89f57f56ddbba6717266129fee2f9f533a7ed38b8d5eefe961327343526f5ebdf6bde738080b8d907e8fcb5d7337fababd338a5930dc0183836707f417c407cf520ca159fc163f1194f27bc710930d0cbfb27decb184143d7dbce0fd8a822ba76fd72324f37aa48b69dfa2af961b23799c93d15999c986c4b66725dde6434f90479c2539cfac09457327546c3f4c9921fec8c0fa09d4a1958ec20954a49d9aaf48ccc9a4d07cdbedd0699ea5d0771d1cca27de2affbbab79bb975fe82adab7ab669dbe3b905cfadce6cdb7652db76f8d91999cf2c1d337bf6980ec30d99335e9e3143cc4acfca4a776465a15f1d29cd5bb47831133074f3fe83373346504302e406e8a4e8dacb6d70a45c32984f45e2446cc34cae10192cc17030cfca9581ec9d928afc31d2b428bd4ec9c91c6919d6b49abd06d15f9d46ce58f3eae255bb18716ae69665738734eed5ea99e7baa376ed0b17ccde7760536b314f8b16beb4a74babbebdfb3ddbbe27b19e7be3357837779a91c12100bccd6f3d576f11a024d633b5b2b7e133dceb3e2b7b5bfd56b6d70b83f11a7c88c21da170cae0c2419a891b80d7e0a3dc2946064703dbaa4e25687d6d65e16df8006deb58605b4199c05e780dde4bf17f9dc2c4d4da2245ef1654c432975aec86d7e3cd14fa8dc05fae4e46c7f8e07c7b17631e54d4a79301b0dfd21c5abc7f1ff7ffb123afeb1a8b725d3672dacba2c941c6ad8a6e45ab0e2e6535042d84a27ba651a323605f328d59654f53673ad2b1d5acb25f2c2f0fd89dac51d5e5bb7fa31f6eddabc23bb6e285359b94e11d2283d4ffe43e461a2c267abdd23e79b20c6c810634fbd911a5a00cd0066fc7e20c933fa2f4ad0d3d1d9dad81f6d3c019fe2fd06be1723d6d47f281d049d5d0ebe03ccaaad376041b00cd3fad861e03ffa00cd0d76a5bc53ea2821eaadb5f5ffd4631ea8ddad77923827f4497ea9137668a1dd9cfb80a6c012bf57d67a38fbd7f7bbdd2ce6eb20ed8028d249ac153d4c18fa93f9b5a4d85dad0d351613dd011ff02bd1636d503ade403a165d5d063a00275f0f7a91a5a5d4305ef3d00b498b6df4cc2dd7ba8a67d7f89bf93f3612300e01702a0a783a306da5f38e0e42568ef0f00b87300f45a6fbb7ada964bb87b3f01c04328ee12f418efa41adcaba143d847d51280ee62453924cdc7214f6b38a486ee3e0ea17b4fc95a600bd87d9cdd07b5f043fb77a1729ae4f5434f47f7eb814efa17e8b5f0537dd07c20747235f43a78805ad5813607e2cd7baaa1c720036ae1a74b35b4957d24a4f8e84277dba1f3aca5afa74d6ae6995f763bb53edc6b434f476535d0d46124d0ba7f815e0b15f5b4ade703a0e9ac6ce9e3c74735b3b21a3a867d24987db8d31d5f68fbad7dd814d4b4efd713ce501f3674170f1eb0053af966de27007e687fa2d3d958513ff47494570f74937aa179583ba11e581400cb5556c3ae1bc3d6816d1a047bbe1a8b31b007c04f936ae814f44868e9a38984c9e0ea378ae14f08adf34653f6114d9792375e143b32956c4f6c812e3e49d5964a2ababb03ed53371fbdced750c0bfd2cc99e8a3406de8e9a85f3dd0c67aa179583ba41e5814004b69d0cd4783b53534a8866e801e09cd7d72ea02007a99f3620b14d1d63baef106491202cff930d90280330360a71bbc41328aaeb39160bdd70070d36a5822a32cdeba2dcb25bcbd67007047aa018a7c326a80d71b24a3087428aa9151743536e58c3e12670c67ebe0a2f35185ae91a614eceb1b9d6b35f4f6476b9cf18afaa1a7a321f54027d40bcdc3dad1f5c006e241719660d7e5b17560138360cf57633106b6d58c6435b4193d129afa2842d778524c727cbdbc5883b7bffed0a9f6e15d1b7a3a1a5003ed5f09ead4d40bcdc3da61755bf6539bc252cc737c98afabc1bc1a3a1a3d124c3ecce92a44da7aae0f17430d2efea588d5985368da7e2ec5654cffead6a9d11f4bff370a8f680619e8ffabeb17288628e8071c7d028820c6254e069e3e0faffeb7094a51167a03b7c337f14f8c8e69cb4c61b6316f3157995f5919db9f5dcadee0ba736bb95ff917f805fc1fb2c1b249b2e5b2d7641fca7e9277973f5438149b153f86a4844c0929098d0a1d1e3a2ff478181bd6306c46982b5c1ede2fbc2c421fb134e28d885f238d91d323af443e51da95d9ca89cacdcab7959f2aff564d56b9d5c9eaa5ea4a4d5b4d9916b42db52bb45f6a1febf274c77562d488a8f37a463f51ff5eb422ba65f4cae8ab31da98e763d6c61c88f92eb659ecf0d8770da986cd86bfe3a2e2de89ab8cdf94c02628139a277c9f8813db262e4dbc69ec6d9c642c33fec7c49b3a98a699ce9a3e307d67f226c524cd4dba674e30ef303f4c1ed6c0d8605d836f2cfd2d272d6e6b7febaf0d131a0e6cb8abe1c18642c39b0d1f373234b236b2371adba8b8d1de46671a7dd6e8a746dec6f31b6f6f829bf46ff275d3179aee6ffa51337db395cdae34b7365fdcfc0d5b135ba16d8fcd65abb07d66fb3105a5e8527429f1294b5376a438532ea57c95f23855951a9f9a9eda23f5a5d4b9a91b524b52cb533f4ffd354d969690e6487b2e6d64dabcb48d6947d2de4fbb9df69b5d6e4fb0a7db9fb78fb24fb1afb2efb39fb25fb37f677f9aae4e6f92de3e3d3b7d62faf2f49de927d23f48ff3afdb143e568e86807d2f863b3baf98899786864ab3f21464e59a0e257557372fd04f579f4c4e0f94491229f0a000ac0f46b6056431ef8fed053e9c091b00f174301fb33cc6417c004ee0ee4b3ffc04834125ec66fc3727c092ccc14886357414fb401e2b1031a6003ac639e0335bb00a6b20be008bb0072d90530845d000e7601ec6017c03276013ccf2e8049ec02c843bfc20ae48446ac0abab279b08d6d0e2b988bd0436683315c0b88639f4209e780995c5b28615743093b0e4ab8a930873b0125b819bcc58e0107c74209db134af82a28219f738b60061746afe3d99fe1307b167ab31720954b81d59c1ee26431d09a63a1257b1522d92b300027c206a62b34c489a060b2a11db30b583c1b5ab283603c3b1f56b329f022bb1486b2ad21175f8456ec7c18c68e86d5e83758837ef09e6523600dfa0d76c9182862c7c06a761d0ca6efad86a1f824ac668d3014ef06059b0f05cc3d30f10c3461fe8646cc658862fe866c7c12daa13fe0383e091a6e322c457f40113b0cf2d98330983b01f9ac1d22d14fb091fd1e0631d93092ef0683998db0913907a3d809b090ef0e839823b0167b6009db1986e387d0013f84367809cc650b6007f3033c8ba360237a0ad3f04398c3bc0a73987218c5dd81febc037af32d612273193acae6787f673b420ea11f5b0607d9f7a0883f073d590116b0eba10b5b0083d95db0193d819db2fd305ad61186331550825f87d9cc0598cbb58357f8b6b00e6d8761683ba4a0ad908c6fc268fc05b4c2e72019bba0901b049bf028188747410ef32a3c831f4206bb0086b21be0303b0112d92268c5df940e5f3fe6e087d09bf6e72164e08730183f8401f82164e187d00b3f846ebeefebfd8ef2b1031a70b92023fc54efe1809994b7020edccc7b0f37030137f3fe809b793fa9e6abda07e1a98083f214e19bfa0ec247ad2137f0404fbc17d013d8829e78afa127de33d5fc53fbc88782c083f00e4449f35d364e0c0350747b6210bb2b527c32bfe6ef6760d815681d7000dc0e8ed8fe89d295b90ea3300d3cf8ff1601f5489ff53f77e9d5ab0bb4034852f320028c92edc06004b4977cc766710218c1088c4feafcaa50b000186386e7643c661896c318b35cf51fc61cc7b1729ee7798ee379ccf11c7da04f729ee3308b5996a1b00c05c772e99ee15819cbb20c6678cc713c8779f2ccc9588663188663316658562693b1728ee738fa1256c85986e565041f86617886915a96cb99da7f18f3be3f09cb9a67e9931afc690f7c7f2c4bf71a948e3860280dc83504008d06163d07002dc0082c440340382c807d700c5e87b37001aec17fe02178512aea8b2fe0dbf88e516b8c352618938c166396b1a3719af15892daeba583100e46d807fbe1757803cae122b8e101fc8a52511fdf9b1a63b4318ebed932e04de4fdcbfb2d4479df8310ef3900ef6300efef00de41de2ccf25cf49cf897b0beecdbd37e7decc7bd3eebd742fe75ee3affeb8bbb60eef34068074001802ffebaf14cae00cbc0defc0bbb0184ec325b80c57e003f8103e8225b014aec23570c375f81896c172b80137e113b8059fc267b00256c2d7f00d7c0bdfc17fe00114c02af81e7e801fe127a88442f819c2a856fad6f74bd6805f45943ed241c6c108000b087302c03edf710a00f603c03100180100affb8eb700e00d00380b001301a01c807a31a50070c1772c06a056e5351f9cdb779c0680eb00700b002e01c0a700f019002c0180cf01e04b00b80a007701e02b00580600f700e03b00b80100fff11d2b00e001003cf4bdf7abef580900bf01c05f00f03500fc0d008f01a00000fe01000f007c0f4026253d0a01c0eb3b7e0640a9d241bd0be22ff405a0bbe15c900ee225e1dbd201c500f88e7410cbd5a801306a01603d80311ac0180b001b018c7100c60400e2151993a403b603182dd2013b008c2d018c5900b013c0d8513a603780719a74100fd5784c3a602f912bd201fb0468da4350f4ce7622b436e734f22e1796c53b15ccd021cd04d4d468ec94df5140c39a09b8a9801a9b9a094c5363678169d0b94fb639c758682cec965768ec6c1c3d3c4f601bd06b9f6cf3c8c21c9b5180bed9f94601fa659b84763986eadb91393959cd049634c3d2660a738c9d8d637c0d8ca10d14e6d83ccd04ae690fa3c0587a67bf902d2cea6810da75cc31984cc64e4279ef6ca1bca3c19493d34ce0ab71341a3bcdcf8f96b0953515f8c6cd04b9d442df6ca19d41809cc242e9c96c121615161a0acd39d5cfe5c1cfa711d4fea05de00702d3a0d369b4a837fd6691d964201f984d66538ec194d3b199a068daa36f76a78e069329a79910d25468d8a99910da5468d4a99910d6d4694505c6c2bed9a5ed808511a7e550d02fbb141a32df4fca3108e6bed982b1e0b412aa3f23bd0c6f2ab42b386d8441d9ce46d0d1500a8d98ef3be63413c29a08d0045c48899b20970ae126e82d55564a038312e44de02dd4f599e6262dc89bbc85b3bbb66848ef983ecfa637882277ec90e75b358921775c46d3a4984872c74fceed9c662077b2f5335fcc32933bf982977bb78c25778a7e9d322cb4959019c37a64c493bbd0a5793d25b8b0cf5d85235a91bb70ad324cc193bb885669d63815b98b6c97de289ebeabecdd5ec20a5cea50591301a47173b58d45535d43c9692139d962d134572f729a484ec5e4749c9cbce494188ba69337a69337a69337a6bb2213c8bbe4f4909c1213d034d750722a26a76be4e425a7b60968ba6b2239d98c68aa6ba2114d9524ee6e580485300b6e421ef48441b01286c10048c7ade06d70c15aa820bea2380cb4782718191384b259a065d780921340cb47830e1d04155f0e11fce7c0fbe4a41c46110dc52a7cb25cba471006437cf71822608cef9e09f89c0db8e7c002b37cf73cc4c11edf7d04f483b2ff87b1b30faea23afff867776f424820dc84181094df1dfdf1a2d680821242a0666c0809e1c504ae48804ec488d490d080b6b44e0b2332c581be214a99612a566d6d1d8b5ac7585fc062dbb1a311a3b409903810122eb99ba05566fc8b6f679f5cae0102c3eeccd9ddb3cf799e733ee7dc739e6773b3973ba9670deb6960152b79807544b89549dcc22d4428a19e7a56524b0d114aa96305794428a2965a2254244badb5ab1ad65243030f53c37de451413df752cf3aeaedee4a1ea2966a1a889adc5a56514f9dd9cce316263395e9c91a4c3daffccd176808acae630d054c64223fb03d8f6ad650cd0a1ea0863cea69602513a965152ba8a1ceeab796899453ca9d14338f4a8ab9d9ec4fb2d5289890bff74d5c75dee6e092866b4f2a1da3dfb7cdc3259db1ccc4a198e538ac673d0e1bd988c3a33c6abf64b519872d6cb1b70f6ec3610fcfe2d2480f8e69c3d6ccb0ad8f8ee908ced299c44c42df9955be90512bd637d4326a6543cd838caaad5e57c7285b35097c87a0d483350d75a425d65887107dae627a280f87274cb6d8b9ce7bd17bd199e01478af78fbbd16afc9b9c375dd34afc5cd7447ba79ee1deebdee3aafddddec6e0d8d749f0f8d74bf0e8d0c766f9c372534c69b171ae3dde7bde8fd24d0e3b578aff4dbf77b4d89bd25b9b7f795be601f131a63b442a492c110863396f14ce0066e64229398cc146ee376f229603a3398c9b729a58c72e6731715541265314ba86219df653b4ff0244fd1789e4fb38ffdbccbdf38d0cfaf69e63047384adb79fe8b6fbd90af16421490c374b299412ecde4e2305c112611562ed9ca65aca2e49dfd9a7c7228563b258a335b714a15678ee2542ace22c589aa8b7bd4cd12c558a618db14678772d9a95c7629973714e5af8af2a6a2bca5286f2bca3b8af289daf954ed1c523bff563bff513b2d6aa755710e2bce11c539aa386d8ad3a1382714a75371ba14e7a4e2c4d4cd2975d3ad6ee2eac6578c1ec5ec1b15ef5a1a8cbbc564b251bf25857c1da24027982e9f196aa3596d84c8d75e0ad4c30c1da459074923a416a27a8d2afd93d5671b794413d8a8b96cd6cfd9a23d84184a0e456411d571aa196e39c328620851c5a926d3ac77924a9e7a99a65e9ad4cb47eae5a07af958bde4d87741330827746553a218b315a35431e628c622c54cf736c5d84e982708f324619e224cab621c568c238a715431da14238312f9cc964fa97ce6c867917cb6c9a7553e87e573443e47e5d3261f8f3286b1985ba9228b542ae5d3219f13f2e9944f977c4eca2785b03691ad4decd026766a13bbb4c9dae5334d3e4df2f9483e07e5f3b17c322827c27c222c2042a53670b77e49151974680327b4814e6da04b1b38a90df64ea40c3caed70116eb3455fa0c8fa1eaa2485f51add3b894a9972a7507b38c3e2393741d376699fac8b895a99ba85a58ac6ee3b55a2ff1883e64a39e66b3de658bf6f31b7d12cc0bda8b431943715316047344ca078332b9c622a06fb6ecc4dcd8a1733e7dd6e5420bc513c74e1d4a9c75eb54e26c97ba937267405f0529a849a7f5533d939809839c9f81de04bd30a08d2f92679f278e3f3c67f7ca36edbce0baa72fd5693b76d913de2bd7f6c545396f5f56fecc8525d571b18e7ef25f5e94f3df8124fa740ca86998ddd93180eeeecbd53429d5a5633aa5cebe9e54cb6524f75bba4f1d6ad13eb5ea19bd2e5f4f9345b6fea1e3fadce23f748a2cdd4cb63ed0d3a0dd3aa1eddaad3d7a59bf50a3ad87616dd57bc91e7e95611a475823153ed7efe7ee5d41fde3419f2aae9ec0ba3aafa0bd274dde46958e5d523053cd97d0d07159fdfe37e9956c89f1d957aae5dc67b2df16b63b8f25e5bf3c97ea619de9ebfde4bd6336be8360f42dbbcebf02fb672ecad967e9a70c07fd58ff3adba37ef3c2d9bda0ef83ee4a48ff3da8bd0e049f6c352575ec4d9e5d3032fb88eb987a74489f5956e600b57a0cf4de256adca9f707c88d258e9f1a9b669dd06b6abeb87509a9ff031d19f8de0592d1f32e5d5bcf027f3e830c9c602ce3924d361e63b98190fd0fd760f2994936c5947035a594720de594732d77b190314489721d4b58c2f52c6539ffcfe33cce78b6b39309ec62177934d2c6448e739245e6cb54d9ef4707b64798ed11667bb4d91ecd58c6e2328e715ccd78c693c50426d87b3f6f20853cf24837af3a83494c2297c94cc6610a53c8219f7c86328d69a4534001a398ce74465248216166308321cc6426c328a6985466318b419450c255d6b6346b5bc87cb8109554e2b190850cb2760eb7760e66294bc96619cb18cc729693696d1e647ede0876b083d1e6ef8d60273b196d1446d348230e6ff00e2efbd84716fb39400aeff3210e4d7c4c3acd3433844f682195565ab98ac3b491c6718e13a283937846707092609611cc328239649145ae71cc318ea946d0e5266ec23576238c5750fa365ca632954ca3966ed4061bb5ab8cda70a336cca86518b5b0510b7895e23087393846ca33522123e5b1884538dccddd0ce11eee21c5a8a5f5a39666d4865aece1b295adf68efb20eaf815bf26d7386619c71ce398651c738c630eaff33ab9c631d538ba46d03582838d6086111c442b6d38c6ce33762162c44931826989f8656b40f612f1cb8dc9f8a5ef897430fa06152d298e507667c5c20865b32a8a2214ce9d5f1ea1705e51344261c5fcb9110a1756cc8950662543895827a5df9577de954b0aa9f7d5d5af66dcfd0dd52bf856edaa95d5dc6e69a1a5b3eb1e5addc08284a620d676fb74582446424fe22f75c9f31452c9e45696b2865dbc95900ada7d871d5dbbff3bf32ae7b20797b9fd64aee927f3acc92ce0195c1698cdb2848d403295c72d6f38a9893c0f876c1cee4fd42588f45c76b31b788e9712b5eed35f483abfe7799ee30fbcc01ff9933dd17ea99fc428d2f9333f62ad7d9ffd655ee155fec26bb838e4706dd043ff0b0000ffffb02d8686508a0000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff","1f8b08000000000000ff5c965370e06facf77fb5bbb5cdad6d636bdbe6965bdbb66ddbe6d6b66ddb5bbff33f67de9bf3ccf3996432b948be3399c4555e4c0c0001000090b50750feb3110a00c8ff46feef1317fd2506002049ffb900009840f843d4892bc82b0300c8260040f40100fcdfde7fbb16e2caaac20000950700203f0000649cc308a9595e99811900a0eb0000500200c0c0e90f69b7b18da13d00c080030048350080c9f463946c1abb381101006c1300002aff415306956d666f6e030070d00000c20600205daeff0c2acd0d1ded01005e100000f8ff011a4035b776370300781900e0a60180df7ef9a1e5701626366e0020e6060080390000ff50d1a0bf2d4c0d4d00406c170000f6fff8f83def6161616a0800e25800001002004006db01196d61e3e40600e29c0000aa0b00602dedeb6ad6d676c686002099f6bf3d836c0be491c6d918bad90380b413000044ff01460006636b68630a00d2ff69350500204e38aa5c48f6768e4e002063020000ea7f79df368089bd83a93d00c8fc57df0b0080481698bd8db9ad23f4318180286041fe7ddf86541046c85405e7b30604a19084a9f03f5c0bb0418f336ac2cbd334c34ca39da36da245a21da32b3bddde2c7a7c9470dfb0e094527d7f24c331036eeb51f3607178da584377bea47229332d361bffff9bd980073e5d2b2bac3fc839cbb69571e9f66ef0eda83e5d1103c30d88dac837750b045075ab77a939ae9091d95edbe1a58dabd8237af61891c8071263abbb8fa8333c9173ffa8905103a2a357ee6ba112ea7920277ed989da81578267febaf9b523fa34376890c9082f2efe6428d426d1a6d626eba6e326b488b38db68dcc80d263c272f0cb487f4e6a0ef24e694ecf2560a56d7cfbf6e3e55847cfcee8ba22d7bbd9e3faaf55b91b15a80b9919d901963c154d0373651d75657dcb8ebb37bb8108bb112abbb163cb23b6f554cde530c1e554cfe5c817de2031def01eded8dce60c18efe080577fa9d7f089575faad7d08ad740abd7c8cbc7b8ddc734e187247934eb9c0c9cb2643c85480a7bc719b101ea12aa672f449771f88b4e9814dccf25e62a91788c8241b545a1aa258144595e5bf1e686c68ed52197e62d1b5f9e5944a51491a531c3df27129d0bcd507cdfc89e93fc4fdcb96b27cfc1de96fa9ac92b552d3895cddc95ad9033764d14562d18d29aebcc29cdec562f39f8cb96bacb57dd9eb4bbf6de994d0c486f97b06d6af22cfd76ba0366528ded1d6b4a742a5a97e73c35cf6d4a72c5d1146add3b672233dfe92cef027bb0d5fbe8cf87145b43d51ecb8e14ff445b8a570b4b1c688b258b6793ef6a9cda95e22e730cc9acca53656d0a9235db555741af698cb2c8ad14f7176ccc3137965c5ac1a6ac1c4b6f5e3c37c19ee3f8ac116ad8b412d9b423a31f60b72e8d3ad7af5df2b62c11962d6497ade930ea7183f9339f38b38cc41a7aada8a4b5871b96cd487fce2324ad063e87f199f930bcd8fc46ad9e1cafee1fae879aa86bbd327ebfc0415ce6c3bbf6189fb19cd0585ea795f020495979c12be0cf5d23fbcec8a3dd381788712637e178ead83eab5fe968f5abcfc8c8d2f41f4f70dc98f2862adf7ee0497ce056b67b8dfc3c9b9bf917e97dc1f08c105b31fe4038cb63ebfa70693fc83fc8b81fab0841db4732998de298a4c4ad4e65965646e33a5346f1b2581eb3b9607c4e8629dbf8bdc99d51c6af5b7de277777c8af1bea9671a777ab326ef7b749deab6f31148907ee3fa43f202d5250922d7627b6e078c5a1f91c767fd8325f97e216eae178cfa1b1e8bc993ad154cd9e254085234fb10d994ed17fe781998f8d340d4078565ef33ee52a7cdace39bc696478891d46362c95ba4ecdf3ba921ad856681cdd01c15ad974e6fb59b735b6f5034c5229ec7814b860ffe60adb1e65f2ae035dcb60f511d49b2eb043731ccee5d76dacc3d70e2ca58d4101526b33f8718155d6ba658ced3add4f8c82773494438a65f7ced2b2359a3858f237a4d52c83ef5772d12799f73d9f3ee9de7d594cf9f1d3e97770a8dcc4abff167db5b18241f0b9d2f3c5b5b2d48da470b0d7605585cf76d970a7164fbf3fd40e6a29fc102c5b03fb9259af95c6be5a64b942cbd4f5d7002c5a4b551dd45bfd5357f70ba4228a005af0c35ea6eb46c94b56cc99528da2016b904aeb770855f5f3eabf7ec765f341634f64cecc4714bfa678127911751230762cb4dfea48837c4712c638e8570328f7f342a7f3443b73a1bf0c0fe6b863daad6c212a6cb9c34906580556a026fee3ea2c1ff10e8bc6dda82c49c5ed4342a19d9f05844cd874d4b2c52c42352dccf0883450f332c91529f609cc59afe1324b1689a958a51b761328e6032c9763281b9917c068f8a493e6132150dae686f6d3e87ee1911b3518c64592ae249e9210bbb452ad926c1b0138dcf71dc532fb2707ee15d2f12757e01ddc3410d74fa4f7a1c8dd21e3587847da427f8e609b22681740e6517d9188de5add6d5bc574a1a033556bb4149b7c110040dbd462989ac6b55a7f13c848898d4a90bddcbfc160ab36a444d8a737354259b4ef5f16f72f58976adb5a545e962d014895da578e9217af47a395076891cd904fb66a7d2df96ea347522c2cf12a37a309ca3c419d71a7f463a78d436ebaf639ca72516f013afb13a0c4e03b03127db0da43eb791db740f5d98045e45fa21167c415effe2f60200e0b62e1b05a6e467881492abbfc6368ad0c035da62b3318cd00fd16083c0238b599ecd303d7d8e8eb3fb17f226cd1dc2d4f9df23fd4cd41e0be78240c48cc6cb2638721a404888e517f20bbe658dee5528e97079718cb66c0c91a4baab41c6ec974be064c6b843ba335b713f75298c0573b56f6edfb7fc487f899d9143569c7ffab55ec1700854a7a4228bec5441f8eb1d768520b9bf7fccb86022683258c5d38eb6d21f627ff692d57d84e27e396e691bddbb11ac248569e28e4981e81d81a9f079953f41f4ca89d442efaaeb75258cf2ed6821c40b221e804940387d330d44e926155b99d7bcfd6e168b5cfb6e024d4dfb5e4aa72f2c215e9b79b0e6b1db6e3c658efb9e2a1602c3c71e6a6d093f35e0b66e6c6066699cf5f7eff8dfbfdc914b69fefe8a04c22cfa2612c646060680d14f0b03030528a569c69ecede5e9e4030b0c00424dc0101c810d6674d197858f8a80721075ab52a5c2c34bf386ce0cb10b6fb3f8d8327c046736c8090bd835fff6ce45ea2586956eee731cc68a65a6c3c771c100cffdb99d1e6769da15d9531354e24a8630ca4e83b2f45eba325bf0b2a882ef43f8a6a79e1e81afa5ca0745e0413a8532bb2b773f8b3f971bfe0bf4303f2257e225784e2e43b6a681cbe908108c0dc4fb68e1ae15ed9cf22fc7034add60811e8ef25fe1e69585f51a276cfd179726d587b19a34848e58a753c275fdab9f26abddc9a8ff46678ee587d306a8dcc6660efd478314a87939f8e4d060592847eb1de575737ace35855376e0334574d13b69e97cd33828197ae7f8542f1d85791c4f19c27ff5066b12e32a6eafe59625bd962dc84e6d872d8c0778d769a898deb80fd09e387e6136182348ba979f82bc5981fa3800be3979a01137b1e3ebb11ae9aa1a709e0b20bb0ef1a989023ee71baec83cb1911c9ede13d1ff0d21b30d2efe3c4ec7a98fcb4dd15bae48d821f20e51d31edfa2b0d3f42d33520db151b258ad22e5ea1638256aec5d5cc94f1f83caa315f4a7a20911b1d45f911ff404981f435f54a9808fdea086b449d2f6102992f6a4291af25e73c49b2183649ec1433494e1b3559387a13dff5438ba3a6ebbf43941ac806fc8008c00d58044c0039400b08050c003580159417e8021a801860100000b42f03000d341320022306e0c0b90134f0680009a20e4083c402d0414a0064c8bf0022e41a0009fccf9ce9c68088830b618598af575f574023919a555576b21c00d0b42eb178d657863d0ac53c1d82be441843388fdadb10cf7eef5c2f944a8e9e55b0c7d92faf6e5d061e01b66634d302e5c73b846e02951d7d9fd9202b4a2591e01f7d06e2813f9285f28eb084ecbc8c4913e43a2ad3138d5f3f384556ade6098f63c450685be1bee5aa85650a3ea7c3e911faa329ac5963ba768b9159d7f2e652fb6a7224c04fd2bb5b7ea2f87312e727c9d48fd3aa852b88427a5e89e5da452e5cad7b4777e70b995b8dd21981884ddb6817297052ea88dae87f5eb1997f0b7de0129d16b331341c2b894a590b16911c612a19cd2e256e96f81490048245fbfcb62f63bafd535409a72795ba613a29fcace2aac1d52cbdfc63b9960cc9bd9df6027599f93ec238baad8abcb379c98fdc680cb28f9bf8016ce02d1cfb3fe1400050001a00fdfe067eb8ad57694169a0a9aff09fc8ade99e63a4fca24d5265c737d5ac236b194c4b33b37b9589a21360478bee2e0c2ba857b24e8b3140715981a580d5bfa4d3308e624ab8d714de4ffdeddffdf1321dd00916eeeb4d90d6e9eae272cc7792739fc38249860542c44bc42504603b2181e733cee1177160455d65097950aadc1656e1398f33b58166cddae4250e7238c763e04351a11aa032bae01d62c0d4e97790dfe4dd216a99da43807f5e23dee7c0a84152ce750a07f8d86d51500c2391622121fc1ccc03330e82e28d4dfc947c34d78a10da41f5572223883dec7acf29ba9ba450ae4430c93841bf97a2708780ee527d879a5621830e58f605a18f9305c5e64ca5e5022320c072cc3fbb09dcfc891a92e78cdb04eb42cc4445cc2b13133d900a17f0ed4ee66ebdd1629cf827d0a2e77dae509e97a94d040a004d2f88d73005e22dcb613883dd386090d088f7b333604bf35c7fd3ac5e81578ed1df81656817ed33c3ef895be2e8c60a98df8321a5c112e518ba650aa355ddef231821059edf9daf89ce50f04b4639274ab0871ffeb625189c9b71608f8db5f7b797a72ed5d8b39e4e8d2404a3493ba4afde69f5f730978010c0a1c9d2ea669169a7edea87f1300f72288148250ac96fe5369261badca1bff5ed5cabf8d05ea30d713bbdcaa1f1d816d5e590aa77bf41ef56dcf034bd3950ef54557bd1498f5db77d64af292db4ff44c14b09b69bcbff10883c66c82f0c430f05218aaab5a1c3398cbe652c723b5a61ea737a22196990a44d4d183c6f0b70f630578010387f9d3315ce159df4cbf8276bfc345d06eb137520be12d8539ba613fea7a58d53dc48a961c016f1078736ea41557d0317ad8e4922a3a6710bf7eaca739dbda77002a3fd578763ee15cde87683b9abd8fa225764ddd4b2de884b351d9f720337291f54e997714cf74479eb0e0ffef312b9f452f811e1249ec0efb7dbf2d74238f98ce72b161dbeb4641628c180a475c262a733005c846c5088a4295853cf1d82a24081a13659b64a46cf5604047ddacf2d55e8718cec5417d947b9727bad2ba09ae77dd763f4c6cbb1bb32daeedc45037dc369b0331182e63b096fa7239a8a692c07c9d66c5ecbf8000fe3974a2829534c39086a6241b25dc167ef2e6c82b585249584fea34fdf07c42f69a8e753febceff5af221190716d580b048107996f901917425136aa438588b454bd26b6c6f7fe8000daa948d2d027cfe721c5428f952c2986cd8425beee3a22691fcbc35b1f3e3d1ca6ae1b3bdda1a61aa2bfa3ebd4771c96024d00eeaae19dfeaa080bbba5da0d28b2354a5f34690b3607e28b488799179211e17ee057be063f5617a5ecb674865ba8bd086a4d9899873ada1b1a3aaaa9827554f98cab9aa22a732b460da32d3e8e8c514b7ebd860db3b29e34153136dba65b10a3b4835282ba2ae98edb7773cd7bebe12561123764ae478581d9d0d18a079ec7275ef1bfa10c6c276599be8e552ae51c29ac3baf9bfc36cea1d7e1a4e42d817f456e737f2fb719e6d6e0d8576cdb59caad5aed7233fd8291593065dc95ff199e8ef3574339793928501375d10ffb89870b78d83dc69f17fc009b38881ee1256c2c567522f701838e3c1521d32600839cf56721dba6a29eb063a5d03621b1782cb18fa49816c390fd7ef9685bf2746aebdac1fd1359a21e9fda33d45a77b3c042312bfaa62bc01deaa3cfea64141b5bcff0b2cee9327308f932c828ccda425638162f1146c7b569413666f7d130dbe2f303bbcd32cd666faabc6883e3d1f9adaf11b57497018bd01b47b9eed74cf8cc3c0937285246fc4a904f02aa30d17ae73d89b6bea2e66b04056db528a7b3f5e290789d78b3548a4695ad69bc0c8e509de10f8f37262f99567a80d02b2a8793b3d2e9dd7a2fb2af1dd59ec8dbd56b7be231559d3143cf3b9dd417592d327c5f0e3aa8e4afed313879dce8e989a9d421cb08561524c60bbc4b422c979313157e3457b1b03d93ece0c190c6e8cd5296deb0e83fd826a4be9b23146fc9e01dccb6021ba938984416253d6457f8d2d45cc9196a0db6fb9f444d248a646997e020517f97a7832b90860864032cc1f54815db3f346934cd32414bd37a32dd9b0b0dd7e76e55c4c87a77c81f3e4c8f61ac4d3b9b10f1e7d7bb973b1c4f96639d2f476f0cab64311fa55c5841444e7209d6e4e9d49a3ad913f35f1b8fc4dd533207bb98fcdc8d191754a06e58c05687402e8df477f77bb0f25cb115f08b550a4ec3d952a011bbe7720c58d7d0be67b192fe26d042ead474f8fceafa58884d0d4e08a6d9d6114c31a1d3248bfbe3f4632b16b169a79f7e3525271f2ad0f763ffc3d423bfe2fef2c54f015cb027c9c51ffeaa7b940c52ecb78c90e07e0cf17027b880006540d1e13b56a5904353432283b42506133ba63cdd9a0334520810e642d10aa107aa8aa2b6b5617a37c3876e2b64b7173b7fa1cbddcbf0c8abdf3e85d1fde2cf8458158b447bd6a4020a6b146551eb8e63277fdaea343fef4cfed3cbbd2a622e1bfbb0f257d1d3e6d4e7bc9a9d96cbe133d70ae6231c3734925e7935ca35a3d71fc68548863421d89a919520d90cfb35628df03f48375f78f6619e2cbba2aebcdf8fd406192c1a360f7db0c560dd34d3f2f2259cc1e96c733c99aa75853452cd152e14e1db3412de26876bca51b66753593a7299e82c0df38ebf4bab7fde79b9dc0f4fce2dcc5bf9c71434177339326f6fb651484b10615b424b53f83c0e0cf9f69cfb904d73b97ef7d2df64353969be6dcaf457cf82103d9bb0e302c4b322d9c85e42011391dcf185436e98777b871f505cdf7fac8e04836da8d5fffc33abdcdfb518c10a141be5f4685a4c144ec1bf9d1c01db73e34bf589ea1e81e95d6c221527dced69cc711f28841777c4dafbe57fd937aaf02fc5ef1df45cb7201796ad6f54c8725f37dec79c5c968c6722d9b66c9706f9184a6320913b129702c31fbc44a3902baa0ecda631d4319bbaa5702eeb52c8d6ad858f99e261367a873ee5dbf6277b5516b3efd471f042673331e6d86bb59312a5c78914f953d6b7d3b3ce49c8123d54a2ff5f610a119631084324b0eebfe6d308c5bb217ed4547972b08de3663a532a18862de305c70426f69c8fb352063db6351c507bc5ab70cd48ec67b3db375f9cee11cbbbc0a5e3db526bc1763abfe710f558b364976e7bdc974f4bc257842eefd61f360ca91c18cbcfdc751f4b57173918fd604c6050cd1adda4aef273bb950cafb7489d8b77918bb7207944b53ef9e4d33741884db51fe22cbdb3b3388c17c9007441366cff89e9d6fe1327730d61eaf29abff120c56c21bd9ddb7ab2d19a53b4b941b8285a5bb34c56d3207bdc3ccbac9b46ad98b176c28d2a8d700c891341d92904964656c2a8a2328395d646d166c872e6a36985422539f533a973cff63ae311c2ea2cfa4f24a6ae5e938a0c9efa62c714a5b240b7d2eb5852b78d1666f496f79ef0d406cbf2c9968f633056bb9bc37902796a2a883f727373bfcdaff9945be67fe3051e5cb2d646fb0eb014d7cc59067e795ecce5b2aaf8a7d9fe08a4e0a5e211329b14f0667c126a509138d51a9bae8b2cb6f9c6efada1d36b2cd44f6ec7c13c8b9202bf2ddff7e4df1ee264e91e4a67343a03262f965dd54c8fcf711b4f4b1f3d50f0054ba571872a5d17de9e575f3f739cd78e9a9be3fa980f38efa8c2c00e72fd70528e6c6112056f307149dd3d8cd5f947ef9d055c8aa3751fa219614c67a1404e3fddda1cc88978c172615806098844da72b3ebed34cbd4dd30a4cc9cf2b09bc9aa8fc9228de0f8deff1cffceaacf7dd724a841a287cd0d04709e698f39a294ebde3b058f4ee8eb0dfb4fbc44da341ad54fc7bf8347b69f93ba87aaf9c19ee152074fbc2c928aa6e624d0e5c20b15eb38834a729884bf33481ad082bb598dd9745520c1732a3f45d2fa9779408df266f44d9a2cd8c38fb1abcb0f4f98659f36e37ef17d78ab81d9a91953d171bdeaa949df538021140ef668799049ba1177dffc4b526c941a75e511e07b986e339fb890ba78524e7dd6fd386f4b2a37d6f739996552ff33b7aae7fec8a25fcd97e1123723ddbeeb57c3889105a3b75c609ad62c95adb287283beb10957f672b893b2e60f954e3323873459d05e3a1aebc83adb7cdafb2e7f573dbf48907bbc2d59982fac15468b264d5de21215eda14d9645a330a09950625da1aed75b2000d64d084b5c1891e4751cd4db621dc4a841fc7c97084405c492d96433d53b8f446fa8cadd6bcc0726ad0b4213c45e105d9c3cdd36056aa291ba7ebe04e0d5fbe68b3d87d232080fea02ad3d57e7064a937e411e79fdb6f69af2a9b138ee5bddcde792bedb7a776977d6fb465fecd31c7aff3e4ab36e807c4b94901bb05b6a3580dca3514ea7a20b300c675ab4b186a21d9ee38da4335b435e37b926d0d5ef0373d971018c8ec0a3e12d6a0337a9e793428db96eda655dc9ae831b2100dcaaf3e6a6517a89fe6dd57a04d85c9d1fd30f25f098187f1748773603998b7c73659bf301b4a64e551246185fc840126e5bdb73d2408b0256cd5a93ddabc2205e92e9ac59bf47505b956b782f9776c0a7668211791a8d8f451351365312d23ad2137164b1962bdb7b3e34364e59459ab4724fef87a7f1fe9094617b9eb06f447cfb1f65eebc5925063a9e3ab5e6158be5ef329c1a9372ee3d268b0a0725a6d1d03fe25f88e4a92b20cec872f290b881f5162ceabd60519cc1e4353b24d62817df1a357d0b4cb9c668241aa7b62c114fa165234be2549049ac120432b8cfd4adb4544e62fe581d7ad2dec0389ea954c088929f803832fe527a1b11cf3beea0cdfe17adacdf861056781320bfe0621882f732c2bba713f3056aced455423c4e0a4af0ac418ebfca6334007d5c2709f8d26a6dad27ddabf27e7265a6b7253b3b44fe74845e4e60b0991c634b2830d9b3f6d0bc381bc80234b0869865b6c35520acc9fe35e36604b20ca8b95e1d9dbc5aa23f22251edd7fa6dfb9ac23d35d8f45577bc522509ad7c6c9964e6fcde450422bc037981352f4c9ead31615290764142b6b70a565ba4b19eecc930d839cf269c922f93c7b1702041d89372f44c3ce101f5df083cedfaf847b67b537d74fbad375677ee70cc269e16913ff99d7edfa1e0ea4c7dfe694ef84590641de55fafdaa82129438b7c6e1d4e897da7f4c363e2ac44b51dfc0aeb8b2f443c997c6ee228f476bca7fe86d38269ee06375bc92f7ce478bf91f3edcd3d2abf7776bddc77d64eab75e01ab7db23a9695caba7464f5a35bf8c9969899ac73f154fa6ef3a931bafe1d3718b839b79ae5b78a4120dbf56dd29edb87757aa7d78c9c65cbcb388ee653025397ee2c8da593f392b874312477f4bf79251b5dc3e73a794ad7a73224679627cb5d86fd7007de9cf3313dafe52d56aea1b9711a4ba12170c0329bcde9bd9cb72a3b081fb5339832f9d7683c1394e52d692ab9caa2e3fafa7e6f43e77b464c43045657b656d967aa661604e332e74697d36029f01635408761ddab2087252804b58273ec3a549f8ed8dace4fd8283af7b12efafe938c7c31f87d90db6d081158a49a12ea23496e766e3da5e530fd8d89f0cf73e3008c6d4d6b2d4bc8d56d38baf122c212a01503707a4f40ae978ae603c3114d71947479969bdee37a42f6a051f44cd7aaad285cc70aa2e2f4f03edffa00bee49fa78c0fd5ae3169079122782d458eb917786a5ddcee4d2e3876bf2d550b577ce7dc36eea9b1265c45462d11e7833f9c4b18804de97fa1e6ce1776b8af5d008919a9f58ed7539e205858ae137ebe22ab1bb297ed33150ee5ca286aa9eb0105d0caef8ff4d5aa7961c21090029c80becf051a721d1a8ad68190c5952c32a7fb813657860dc48e28f14fbe368dc50a42d69c72350e45b435cc9743b6004afd35e6b3433addc9f9e307f004ac52b3fd18a66d49948555a7fb12cdc7183270432f5382230a2b22005ff24a87bfd53f9b71d6ad7212234cb0a53481eacf38596aa573b1884355e8f3b2b4bc52d6e156f615d887f333dfe86116371146b7b78fee3329d5658e072efcb366c477cd34bddc7d37b7ca380b556b3499f1808745425429c498908cb20b38e969534c385237bf980a981027c05db611b78f5fc365bd93c0365e41cc263453c6bf561ef820dab329d998c3853768e5bc909df2777bacf84dce5d61f29cee0c4769f99d99977c2dcafcebb9c98488b572e5151b8953b5d8f62b9116d6090342256c3a04d3b4221831dc517d391704559f5c2b63b35f96ca98a56e10f922dc75eb432867a57955023fa85b3fcf35c81c3c7b3fc62b5e4c81725dfc15c4bc1ed15af477cd7ee1a05576dd60c6fc6ee180c09c7f2b456174a3bd493493b9dec9948b47de015c833a32a89426f89c37817dc40a34b18e1709b04f9c248f9c5b8c4d192acc9c0eff4d4046b369ceeda3d5b05423fd403896fed648b8ea487838f7bba8bedcdac486339f8525575c79f70d2b9dd992f72889af39f0f5d3f682809737b185d5dcef3d20bcd7a52cfb939ce6e37832fccb27dc190d681b07658f91875b8223d50cdda124874bd2dea33b654fe86587eea98842857df94a8571ded89b4968855dde8db206de660a6fa2562c5a535a5237cf5b0753b93590ba3be61a3f6d65a23a4c8b865bd869da29dc46ec2decf6bf184468a67b2ee4a4a891ef49ac5c1234cdf2c062f88511a60aed0497ebd4368a092828d58054a50350d0c2de9dfb214f13f97feede3ec672ffdc3d73d95ead38e8d055ce55c20ce57c904af0811a9c2a0c2419150bb8ce342811d4675bf1be92f69e5257e40b2ff989da4180f4f1a2762664860df4d065fd8dc8d412254410f5d6966e4996664300155895df25e7d436cd0069bd689cfde7b27df905a79f9923e557a39be9f9bd2d890f122f5ed9e2294c937edd552e8a863353d98c32ed82a0c8974af7369cae8c5a170bba85929781c974b36bcdea5d4bdead7a50931e26b0ac53ee6cec213eb0d4f97e055723fe87dda09933cade505970c7de2824e693efc1941880a9ccda284b79b8a84f76ed7cc81e49c9cd87e55246b5e14a40ec9c9a83a5b798e140a79d5fecbcd0fa26985fbdd85db175151744dd1ddbb105746b2de466ec981698f70bf405796c955b953256f490b4547e5b666cb05a93f425f0e5f834b8f8c304927fe905aa003978bacfbab01e3bb9adc47f099e0d71be298074e67ebd996ee195b1f723fea5eb06d2e76cce3dc7c7d8d040e31eefd12e19da5a679b2964e4f0d65d473ac946b86dd2815c37707211fb7077998a3f44eed21a77e32a49e6cf950c84f1fca5fcbf4a0f5a3c937bcf24a9adea2db2edaa7eec5c6bb59975546473efb84fd98190a6d89f0b480bb0f3e111244b2520ee20590f2d2776c701469c35b43ef7f0fc35a393ab6234462de154cfa9a423ca3b9fedc8ff3ac1d57b8b952b1f941ae1864ff3642e8fb87a578a72e77c83fe1576510ee7ee42c28819fa6ed44e00f2ad3966d6cdd54775af8207225536d042b4723c5360d2242b761730175d5b36141695dc408250ed86a6a2342f303bc0c71d7f7ee545229c23123e8c6b4649f7cb60c0e38c259a8c99de76598c18b0d4a79bc1c45eeb3bf497ce6439c389c0685cee89ff60e4e3751f89ac19e6fddec96a22f7aed602f5de2220d50e7a93f7c8eeff34106803094e042533f240bd0c2cde1d112bc1cc5997589bffb89b17662ec61bee7ec6145e868b03845b4f60a85f7db0514685db1457b9b3c54eb167554f85e60b19b468f2fa69414a7115214bbf3e3094bd22e4f97adde735a466db23355197481278db41e391e45f2f21873e21172a0fd02ab11aaf5e8568833ed4f74056d4bafebeef108ff62c6cb9b7444b75be10ec4d9046bf43da1c1c63a7fe8356b1f24a91a5053e287d62e61482b23467d67a8c97f395c4deb370dada3534d7054c9e7b2bb8da6205b88848d7fb33cec8f5045c6266bbee01d69f924ef7db4c223c1c6cb4c20cb66fe61fbe5d64c3b52d9514d1d517b6c95805464a189dd9622fbe23e66216552b5d2ef647dd422da858b9980a013a55ef97c254b47355f4cf071589ad0056f5ecb4f68761cdfc683d5eb47b61d3c969ee6a6ddd84942e8abe99a5148d83ffb3ddc646de64170b14cede1f21b5dec180c6b6d169da4f96f6da4c991fbe149bc89c48059c4f6f750c68b960dd86f8aa18003bd565e0adecd0dbd410d4dd481e7c2384cfab98a74fd1901c70163f4563afc14cbdc34c9deac387332baa67443ac07abcb8d1cf447c8a7aedb65ff1c5da81c94989e4e0a9357482068046454716d920ce70c11e184fe8c52b2a9b75cfcadf42484d9a1b830831baab2a07441a50f3eea2542a350df3dd570ce0d21bcd7a6b3c3d9ca52f1440ee42ea66fa0460fbda546e2d3ad96583c9599d6e697ac001163d233a68fa9875dffbd70084f8b2ade8a226d69741ee8e9c390c4372c5794fcf940ca8a4f5bd1b542e43c872287faa6d99134416d466535754bbd2af90afa6b8bc98d1996adc118ff3b8e47e1874145cdedf419478d1316f7d3c0507bb9a9a72fed94a6ce66a7fe9c9199058197db5ad3394da92f3e1391b3ecf63cf103e35eab6e29fbc10cae9ca31e3491172918565e28cf5f0ba56531c2daf3fbcad53c6618c344247d69bf937a407552b241fcc141e38f71f1f5882dcb8f5d201f5c5909ca7c383909bad3e1cc3a3610cfd7b87f16d38a97d97ee2e4ec55a0e873fcba5dce968b33682b13d5b50a40276a568d7bab966a28ba5efe675372a1aee4b0ba718bb961bc5888cf83e893856b7ac5ff868768240f1f604fb0408bbcdd8a401bddb7036edc32f9152d8081e916b88377332f4d9d52151bdeccd5f5a1b19b723a7487f7b9173e783a18a32b33164306492969adff49cb67ad53e9dd10db0655487076f82b4407d3280e1e7e0dd72718cb154315be4dbe55acd2c978f22adfb0f7f82738638bc68bf381d99410d7df951ac967a5e1f93172f2817a75726fec19df705f995238d3327767ea98bc58cf14d07e31e0925224b047fd51ffb9fb9fd1ed93d828685c29d5ab4986f6e3116a73291ba22b756b652a4f946cff39a5ec5a5179ff5ef3b8b6ab24769696732b7fc002095c2c8aa4f37386865d6df8b206bf81a7e5f9dc8d07a5372b84f82879a1796f0eb68687f34922b9d1bbe504bfcf9f9fba97ee6978d9bc96c7be6e28a3fe15086c1c83cf0a37ca32f4608fce5038b2bfdb170bca943c0f6083165c09d04155b74d2d4ff55685729c574ff4e0f58fc57c5f8ceaa0bb26dbc44944d06165f33379ca341f3abc9b29bea57a0da59cd21717dbc2b979ef4c4068e51f5215147ab5e5e7f6271a7b35efd8704ad46b7a44ed523068b5bf84f61ac8b5c36c5874a5e4e7773fce4dd3140a377cee2e44ae6b191ccc444323e8d09ce219fb6cee8fad516f9ecaf8e6d44b2b9c3097eb0c920254f0cebed63626e2dbfa1c1d15128b30765208d33494ca32dd0e1b6fa4d8cd6ccf36cc2f0a6407ed237372aedac975b35aa2f48a5c8774cd1bf996ac667e5e0622a2db2e4b32cb4df15fe78a7795102c1b402cf21efa18fd9b0f5e9a0d0bdae45448a93055a975fa7e4a908ddc2664bccd8ef242518ffc314de8ca1113b650271bddf278f9db48e7cfb4551f59ea1812fcd69691c4cf5683dc89f25df6c91fbc73c8df0db475b637602b16dd2174642ff9ec3c9d49d6987af412bf29aa96ac6ffd567d9e2b900095d50d8267ebed9539f777ee8a5fea36834e7053e71cc790ea7585e37d10ce708de9dbd8ca7d31352908ec072d5daddd4a5b6b77d784b7d75ec8a3a21e4f9fa9d491aedf0adcbeaba34d73165f7533cd0910a19429129a96548e4ce7edfcb12fd99b8e73d7dd56b9840275ef89f2ce9702de7f2bf67dbb049c46db7c74e8756dd0cf189c2159ba8e11615acb3a09e3bf320949706fceefae4f0ed00bf2b6410b55c726381eb1dc3558ec870b697b35cb6d38499977f70bc5133b445bbd5b36aab8a01025b2cfb6d073563a9c60c6612634a5d86b05b19198d39f19eaddf24f6e2c10c49cd56521aa0cdfee9241b7aff40f6bd3846862c4cef0992cb2e8e17e44f13dcd21d272793d9fa19ca4b58b23c631cbb70a3c40283f0909868120496167f45e6295cb3034288e48283947dd62c2bf254d6bc9957a6bd13955f09f462ffe0093305f1d21c23a4f0910597c515f6ba73edbece50d141492e4de2626aa430c9d67da5790a1e85c2d2c082218cd77fd4d1c4ab0e95d43d1a976aae0ad2051c545df52cbc5c00b0efd03145a1cbb45f0b410f2e802f9d776af385a9eca480333eb3944ba9b1126f8365cbff6778fe0c39fcdd74ed7debf7f58bf79a98669679c6756d264479068ea01614e6f397b66d6fc28ca80ed5909657ebd32ad14c618d2a05060091171f7fa1c42a88a3d73a0ff859691486005af8821aacbe42ae5564aea32d0695a111e9da026a6a017b68f6bff81742432f64e15179ac8da586391af70f43b3547d193b9fbd87b7ea6515762726a0a2da34a5afd8dce896d2c35d70f5ac23e3343d4e12c1d9531f6f14ad651dcd23e4c1ae3639c76cdd3759bcb854c0b1f06511ca5b60cfe98116163d7b629ceb2c6c67c2661dfe64f292c6c50a29373ff8f5cbc379ba55af44eeaabc49ca26feeb7adbc3e81ac2201678820a3371afea7564ff3f8878ad286c6b3da7bbd9895c1db92a5bde42e37774f32926d1b6815ce1aff3d36c62e3181ae84b9bcdd66b27abac531f8c61c55bcbdcbe4ce2dbc09c7ae61279946e39172ce54dc999c2d459d2d08307addab8df737a6282f072561d3b1b3efa2231e2043a2e77b67d2164530085c58185924b1accc4981d600f3b29b881a23c64cc81a5aa39e68b931123ac7fc57771b6596819dfec64ae245f7cb360fed77fa1a51badfb77eeba559229df2768ffbd183bbc0e5bfa3afe25852a04febb1e95f201220864574a6210792218b2b8f91dbd0d660e1c79fb4605143c7aa854b4da982ee3aa0c266555640514d82824af7655fef365a9128992d1ec5e3956fb26a261d705377167090df5ae00a612a3b80ce4081e7d21f59021682e24e28851aca3243323fdc98e7c28f78a4679d931178262d62ec7eed7fbd18504a30548e1a9bcef5ef5160a72284ed15159a196703dab73ca02fd7421e1abae8d3b645aefb6a602a839b5e627f3080cca362f06c39d176e6b5093ab90c3d009a82f968189a4ca78bcc923d3cb5ecbbb9fbf8b5f36566e9b2bd44ad9fbcd8ea81a2b6ede4736905a46cabd3bf9644ff3b5680b1a7a394ca2280085bb0a82755a02a697dad75939f9c28fd71f28625586a41d622bf692ac8a1a89222a9ff29545c28611b452be1fe585b06de7ed683325eb1f66ee59f72999e75537a9536682b3baa0175c7f52c546fe8d7aba3db08f4a57d53ff5d6ba7fa766f8783cf308e385aec49b727517bd5e49f2bf8d097f138b79377f5195374bc9f49171340e2ebab4c32ed1738ff2cf69b146751648398e6a4f3df5ad375ee7308637e7b091275b3f7a1c92aa611c8c49521bc09f84c8dc2908299289c220e2c12acd50ce2fac1f823063e6afdde1232efbe69369e1f5b5daa3d9d16375d4d36bf9e581cecbcbd68284f9e03b2b4dbe12296ad37eb8e763f31632fb7dacc35b77bf6c1c40936a9777a904f4e7a3b78bf933f8e6a0fe93c9ba9a8098b1afc401cc8183d5a268b6e6b0c70071d06c33c84a6ec05d18f77fd62d8434d2258ad235464702030b4841697540bdebe082e19a439ea7b7fa2bce795ad1ce24b90eb0b831d4aaf1ce196fd78ad8d77eda272dbeb46300c16873ba429d3ef9b13d681f233a49cb31848175bf0a35cb2d469905622260eea721b14cb533b985e5fd76a80c5bae845b9dedf011940d3a43c46768f0e0051208340c413ed10c3969c9b8a5711332b6865e664dc8e17f2a4dfc608b10b339723362ee0badd4baf130c39835bc8896c7e5de6608fe20b1e8723e5bbe30f0a10287d96b577ba3486f6e090492e4fe041cb4490095c257025f2373f6a8c978c4853e7b10fd3f93e0bf00c94fa3d43b8e9667d21bdb4a254f18e2066b3fd8d12ce4629d54270dd03922e773cc8e6c56a7a5a96cf74a1d0494f3a9c9c77421ffb6387f37d30fb9acf81609fcd1373262d6b8ed2dae7ec635df2f0c2d855c047432383c771b7aca730a4a8a6e6f1f14f2e8bfd6c998fb1d31b1718e554b12393f807b2affcaa21625ee0b3a0328986292aa3f9fc5f3d98e347aabe284459c2638dfc24f3e46eeaf4024acf27d8fa8a5f2009302f978e4b74cfcb4f7e9f095c3594608c2cdee22d4ccbadb5b8e975b6b2f3b822884669c1840bb2fe20b9aee832a072f1591d438c4482a7eef6f3dcf10c3a58ef7759880f358a0ff987030dae295809d6adc415cea4378da3b2c48dc9eefa7f23086ef25c4917413940b9e1608b6ea513d25f48c3dd696085b37c8e15d68e2a8bd14b73737278e9783cd717932055fc3715411d2a39c97c5ddb84bf5a785f487022d4f28b224f8e2a7cb7522a2beefe7d86ea97e798bb1fad6d350edb8dcf68b6586e66152cdcf38cc4e42396cae7fb1d881fb9ca6f892b5f6fc6c76ef5d2e5ba9d21670c6ee37d74ed8e0f37824da6ea044c35d9d1a3b5cd0c0c9333ad8aece5ce7a2161f06934045d8539e4b9d963661ee9af847c0afa1380a6d2de1d0c975169a28824923bf94d8edbc7ab842750e59344b3642f0da8d1e93a7e38b9f4fd8d0f95c7ace5d9a513c0652edf0aa84fbc7bf2db8581909fd2c76826a6987f2d67db589e6855123f717d5c8bd8272d05e9932cedd3336cb9e5051739db61b6c84343b04802e9936a526923f9e70feba5718c889c8d360ef6904637683fa9fe1bf73ed8ad098d0b0f12c91036be6f6634ad1de9352660db5e2ce20cb576143cdeaec3d97c431051db487ec3f9642d8e7f2a3481b0f1012643135621ce0144941fac094494bff013b40a7eb4720edc0a726edd02258f636dd45c5c99b553c30aa6ba31284469d8c26ed2fd39ab6de89cea5fab268eaa0a32cb2a0bdb4a14817f9fe2b141239712f13bb85c3d66970e49067a687c4f95cc574a3845470bc4d94bae4e33de994b1bf4bace441f71a2d8e476d04e43620f8f0ab77c81df42f268aed489ddbda6e1e37aff470f3294873cf432e4d2a95958a57d6fdae03600dd52012fccb1a488f4575a98a4190b6b0ebec9a5cd315f7b005e279fd91a0cde2ac8aacd6f521997519053003dc9e3024f6a998a6069201635f1d733016ea6b0a2c740c656fa198f6b89f9a56594a45444383eb74a578a4f1d2ba9e30cc1c02ec11f1171334a901015608fe30ef3de441dabbfcd3debc775994d72a5b9e58488940afda211ef8ed851613f156f7cec9ff308c596ae8bb62e946afe98b7657d8c3738d4e77196b9fceeb2cbdc96f7d3b2a781e236256764784621873acd8d74c721ff3e78511284b452ff762c3453fbe9be63284c31a179dcb2b589103cbee1099686ea6df4f783f190c4b289cd203d11a8547c7144a88661cd59769d8ce8ae05c890178265e3bd9ae487f946b16b00dcfbd6c5cbaf89118b5ddae738c60b8e8b24cd5f8b508f1e16b9ca83bf2ac886a848e2cecfa142b70f3c9fe86710e3ba11424b9f53a79be6dd521ffb4d5a0e533f99be8bb814ccc301d2c378c484637f55ff6d1aaa19a268cc4142c8e08f73a6a64b09a1bb3ea14f97dc750da7cbd9d1af40941f72280b1ea1ced19197ac2c020c7cec6b950b2d93fde89aba9e4f11937d7e44fa5eb8ad56e36ec60977c33f7fc14c669566f7512d26a14940ea1890299edfbcbc297c39b7e21cb662874ae7107b310cfbe346a5acaf1bdba7b91adc4c34d8476cd3e30d3275ad5bac532756b0d419e426f89bf4c9b062f0359fcae5ae1fc45095bac45008ed852088235ea37e7cbdaa5f878872890ead2f120a4a18607522c1b0c8f0f3627bfc24b6573008e12e0829933ca81db818f1a6fff6cfe217b7a90ffc78b38af68d2d78d90a6f54c889838e3148020fb133c94901b379cde4bf69706d0ea7ee50ce8fd86ffda9cecaca9fa3d72b411023073295f492a852a3e553b57929dc7bb2b5e4ffbbc0225eebcf702681919054569f3e3258a351fc1089fec927fa062b2925c5e463e4bd3b371b250d7593b47940717e98a54e848fc168440f8a11722925322f026f1fea062aef9a541131b8fca3c85aa6450a2efd50a26795167c1aaf08ca24edc5a1ac07e54f64237f2d8daefec42c2ff0a179ae52f47ff0bd06b58f5aa6935629fadc0a1e67051f3734e24a23d1d939dc2e8ff0b4d3dc4e0f6ff545f7b81c3115e60c93d92c46cbfdeee5e3159df21a142a428c78d67079fe813f514f5f0e40547854ccffc28fe0ecf4bd8ff6c0bb0780cdff95fce0062c5e418c644a150f333c2f3398ff61e0ad36d71e781f66140a7bba20847218af4c52ae7895768c5ef7a6ea3a2a8501442b597ef63aeb4ed102edc3b9ba605890a334c1a683b52f4100d11e1e44f8990436026be49a570fc79288962f06d4fb0faa5f18be0d0d8e6bbc9842858ac36c4ad51a02497f064141e75422ee18172d8783d9dde636d782d9fe1562da76eb31b5e6261b06c99bd4e83878ab85b6b7738d857e7bdfbcb2cc1c45b9e13c510cff59a11fe41e8e1600b3a0607e6cc53d7c08eba51c5821cfc649e29c46d584810fb43465beb5007c861c940393f16991401174eb36d77c0f192c2896edfed8fc7b19880d86e88743187872131d8000ebf97a713cad81291e3fa29ee06f5fd80eb99965fe20204a836780bd096fcc4370327706c1d3bc0f36baccc683103b41c13b43e40a570b575f92c4990bcf2e12f5d8448ef8351a1607993ff653b2a70e8dbc60b0f81e22a949a526a17274ee272bb33eff087b033961744a2e181274cd2bb421a6379cd86da05c1c7d78b6c86aa664b5510742cd41b9f98785879566146bdc755ae0c9803d750f2f356bd8f6994e1d1f7c469f955120b66bc8913e2ba46bf2b6040b8146610433a9080b518783cdf9f295fa4fa397275d791b2a86e1275db3fdb22663702c3a37273b374aae1b266e0f9933cc1585378d882d3ade7537451fcfe55aeda0e80718af569ba7f5371b18b4bf917b587e69c245a57458a7b87f2f218a0561626f0e4941a1d77fae324a4ef02bded690c68a1e28e62e76996cd3359b6d3aab2dd267f847f1ece8527fbf867f78dbd0ddaf6a290a69a8def36073a86dfe13ee11e17fb673f3bd69f8e80e33f394fc6252be82cc2696beff71d66fa526c0734fd97bc9d95e79fad856b6995c4534e2fea4ec036f22309fa72cceb32c9648b79b6f17cb97118716c12698ed3864369b0fc216fb7b66478fed713e086aa9b03240a49f43d4fc884c37472381ffcce68fd116c74c2a263f161697f7fbb0e19152aa4bb18f6e49747846bde3c2e958ecb0ec44c20e088818ab4f253d92781c8198885d1b83e1b1a142619e36fed6b305c2dab134a2d79510825713c23b73e388a6a949bb0ce8dd1f9f43992a8442fcaa917410b9aee4c683d56914f1e9184f34f10f045f3d08a7baf68642075b097cd9a490fbdf4c215dc9d4028debc31770b62eef51f0f487f97f838d24ed3bd7a058190f9110acbc18be1beb6f46b463f4df0ebb7f1c179b5bf7dc3e0ce9488b7eac45a5f65a9f9671757aaaeb59f4e77b8c108792f0eb7671296325776a8776af5cccc8a7b998d9adaddd69be3e8875da55e2f0b81c8ab6df55ea4c2c199644bb7b2618f50c3c0c23ca2317504bcbfce20161accac37377c6bc0d4632e2b1ea705215fe4a89719eb54e785b2b95d4e96655fb2a89f16e08de8dab74ff230c8e53dc4a8e811ba8091e278116fe0963468e5459852ebca81fb9a3d3bbb7d50511ab463806b771efb2df5c80e9f171bb7934e874a9cfec3cc232a28e7eadee3eea3b2e5f8cf77bb5e76de089a757abf1f63845b7719922347524a92a47f75bfa653b5c3dd6aee7e90ea2e20072c0b2be52439abf1982342a9915564b7f801715217a710df33e4fe9e46315476963fdd7fa085448897c044708c18669c748ee60cdd376ff09027a447e89f55d11dccf184d6befb7c589aa49712fdec8675a4ace4038f85e0c7847b2b9cdb98a70ef340ac4c33b48dee33e8c74aa57a8086b2e3343a3783854693dfe9a49a83cb7390a3250e56e6e763e19d24e1a298d2927c6ecf8c38fd896a9cc148d3897c6dd81ab33d4045833812ac95fa8eff06b4ea0a5b249f223cf345c7244b008e4d9ae7cbc44fa3c649850d73e75b843db69b1959fdb3515393b8ce317d9387feb32f3d027ab89e0b3afecb3be29b12074e1a202d20dd17e3bd836812aceaab06e32ec33c1424b51e98947350e5cb80980d42a4b2bd7f91177dfe146931a5f43f513c040381b58852defeb8ea4221e64f5513aa6d2d2bbbcc1e9a479fce02b3d514dde1b2bf235b78d2f1183f7e6a8d7c43c1283d86722571eb24792dac38c36eefea5f52faa0b3b7974062d0b05523185c2d4faab918bd2c6f6391df75fc908c1a15678813ce9961e1fb16d0adbe27b5b98b6ae8027d98efb112e1251f39d360292ec0d942e11ecb3f4f929969518555b8ced801bf689520b9eccac19421e4afe1f236adeac2bdabdfc96cabaa151106ff26e871a8e0da68da04809ed0e7e1f0678b1169bf979b116f096a04ba7baf38bc52ed7168c9d8bad97e500717d0e518b1c5d5f4b4da53b3b5c82e197f6c62fddd7998c8884fd88814c765e5297ece6113fdd88fa76617a3d5e1e3af52fb3b9f28d359a088cc99ce4c07e9311cc6daf83a1f3563ddfcf518a9b7ebea4070a1feab2250011a998e8267c99a4a671b8a88f2a41aa687c65a2a0e0f4cfbe08690366336d499f8f2104ffa815fe150155d9cbcd5af7f70bc4fe8b5ca91c17295df214ef1d2f7ba5b83fe9697e23995b1d6358de22345a556cc23edbcf60efb1c5626d65b378d327156bb255410347a1fbc58f0fb85707d5efdb3a90cf01d2b77235599daec3c7fb235461a8c74c2e7ba43a8bbfdac2844ba90641e0cfd0099b0e44dd7f18b90c9e91d41e63283074dc7764ed1c251cf2c4f87bc752969ed3aa09bdef4b5dce9371e66b947584f4a40c5eb0e34be2082a5c1b572ede9ba27fac98d82c8fb4239c4af19692b6896e9280ff252a73830be19301eeb710750a3a4abdd4c12d2d619a865806ed9b64ae10117719b838b9eff89b0955256226110dc8f1a368a7b0d00375989583e0ca7fbf10d3b8daea24b96022058543b26a20b4bcec1145a0d45173b440942d9e7f6253f5c3fd546585d76cf47ecf586b9a578cfbc03e87513de4a7b8f0fe1a6056d3664e08ced0bc370fa7c9d563f69cf65e5f7062c2d327eb860dc9465cb4db0a7f63e96d4eb5f87bb124d8b37324d3bb13bc8f3dce2ae1033113b2a35943aad3d7c5ac961f2111a14d23af6ed18f111f112d2b1de595b84dbab1aef29a4c3cf46e9533e543ee7fd5a8070a9e59870c05fde334838e8b89161d15b3f458359521a4d5f26bf130f8a1a4781273301ceeb8c4298dd3ee8b6c3d5737ba379ca15fba693a0cba2904ba02649d81c3104544d96cc0e79252c51fa749b044a11f475559951d96273f39649db4eb970f5c7aebae0117267a7ba1f3a4d546a86e464da082e4ac5f825dfe761a543b8ab7ffe4bdb636728a69bed4b46beac39c5a1302d26390bb700935006e5fa4de203e94117f27c1dd93c81f8bbe447a15d3579cc618068e70e4b8af647fe2edcb5e01ff325401b61520f8339e7775bd20cb828812470bad0fc68995513d3a28d106343b46471515aece21da9f36117b54c3bb64f6b77ae218da6f1425eb8050da1d2b232a20d09bbd43d33c40d0f0e8b017979578828d16813a2a453dfc24965e6a8141fbb9f4235c8bc4372bee86211df7d1f1844f1a7498e648d502fdb9fbd541f097e7220817261a26091b737a0e8c977d791d1652d65ff27e0e50fc3117f60b2610a9a6aa710a2f91c2e1361386645354bfdb38e28b6e83ecc3bc5b49bc32a55706e24b4df201356f24f67fdf8e977ccda4d7ea6afc1d5d79f32a698dac07dd2ff64636747dbbf2d51d3bef06a26acbac017d980768357eb7e867f1202554baf6a474f60f077b095db4b708ce568aa542816d2a2519145ee88205f36d82cb261da209bc11486a8c97d798fa8b2a6a055627c77971384bb49f139a45ce45f5d2d33aa79f1c9526bfb4ca6388aa5945a09336c2a842679e1be9ec0770b616f15d0cec089c28a0b23311b9e303e56cbd54e58efeca204e3efe959f6877c23f1f6db01460e0af9e0e137035a9161d3e1e4d21f56fa15968b6b71c7ebf5919851b2cb5fa6dad893abe686a266f1c3850f263f620fe0c7aad0bdaa18fd365361ecfa988ef6ec5815f9df1225ca01281a6e514c5b85f83fbb76568b7e5dbd0fe860765a659d2baf754f7a1a745b6b1e9f81144cb9632c506f77b55202b07981f0d44efb2234bce33f91befbfcda8ed81600822dd4779a939a4f72781dc4c4b3a96463ce093c8cd0fc7c83a8026e719cd5d31582ba49a2e8d3ff78766a31f9b417ff317422a8ccfe40d1e094c8e9ed7a016930d9f36d564d9102f1e66ab446b66b253e79c0cd30b011a46c4c4b30d1557989474cbe58069e67238f92f129c7f936b0be8dab1597cba25ed2e5fafb8aaac70bfd69dfc9aa8b030ee12ffae10abcccb4324256b74c3b688404053d135c8e6598eb31bd26f2c324f91bd42bec7ff3f00240ddbf26fff57b24de8c193b34d186200f045c1a345e541cfff4fe541dc5ae5c1366afbca83c4b0f337a34814a9d3e05159596572b11ae51d968b3ed0a19035274fa661b56c40feed80e2c2c8281f27adf0e1ea0e7ab65677d0b1b5ba835ed8b6eea0175777d09d03203165c5ea0e6a9a93b8e6c8e6b2830b22d0fbf21143dfa455db2ad6e71d57973aadee3b644452624f44e08645a77c8bf30a76ac98336be8803ec34300c304d2f24aa13db88389ab36e8d982f54796369b56aa9d81f972c40dc59ca3e983f96a8362733b196824b6b9d8605430eea8937f350e59b864437951c9f1f71fae881cae1b1bdf7bc460a1fd5afccef01d4b171d38b637b2ee53953caf8b5fc0b8f1230b860102ea661f65ef6f24fffef2e83415196d9b16834ab4ad2f888fca478ea114b927cec2171b06e29d6b4080916434ca053f70677cdb429b42159ef6f8e520b58e7994a1d51f04fd10d41824a454bb06998242c907a93108e59ca74778e139a36dca2bc8c84294c6203676c42bb7193b1d81d1613ba814cb56d1416b2e3a1868ad3628b03d1bc492a52515f28f1549094bf716aed85b3ca24ffcd0612b866d8e8d8f5f109f80fb2f89edb936332f2fb35faa4fec92a7962c91e3a2e2e2a2a2e3e2d08fd161ddba779f1a0b18121bffc2bb49cfdd48cf536cd6244f7bba0d7675500ce66a4a5a0ec5444fb8f21a0cf3324b56262fae5f4c121475e6a15569a22362cc11cdb506d1ef03e72cd9726475f14141ce8ddd53943fadf3c85e3d8725a184be9b56e4551cddd55b9ead472b671e1adc6beca871fdfb8e009a49c35b7099ea82a081e300781fb8db55c2d552eb9959d9fb88957dc66a65ef6bcdca66e72593fb1d67e79d62e76979e020cbc44d20e754aaaac93995d67bf1a904bdf55e71e49947d9bd4eb3f35acb048e24f72a67ed3fc3cef1b22b9162ac23e2613159ef988877e0ddececb3d627f3c9682fe5bca6dac5580d3ae6d36900c44f590ecd1732fe531579a547224a9142e9a65c44399c714bb413f0a46a15e715505fb09dd5e8b0ad4be61ea8a3991e5a988c7cba73ed9a4d75b24ef5f7deff037df5fa87f5f8c05ebcb2b948193e200bc8edaf145aa44c3691beb13a799a181c0c1d58f673000a4331a0ff7f75dc35941b31100660e93dafce6166666666a62acccccc392a53850de12a6d5a6f19c6a3269c3e69c2659894d9d9c9ac7625b7f73ecdfb772c8f8fa4f8752c7ec3f467e8afad4be51dd6dc03bf51119d11758eda4d94a9bbb3ce892a39deaadd386568f58bf52ef1036ab749d46e96fa8c835e70fd3cafc8ca05729ab5a2b1fa5c684b2bcaffce0ceebd83157df067df4af94c7fd33abcd9ad643a7cbd5fd833f14b4e87a4d160e32e3874a93ce9d08d8be88c38efd04d95a94b58ef120f41b749e8e65117f42b7047b1fea030bbbecaf5f95ffc7d8fd21460bb2f3474a918c59aff71c057a1d6ef41cf3674464f75d44eab50bf04bd0eb3877a973e1065675d3ff5992700de62853b6438ed905f72bcdd77da2178f754c958d02368672f926349f32d547eb7b45b97cad70eddbd88ce888f2ead4cdd93754ebc95132dddc3ccadfeb0de253b40ed3609dd07fa3294fa82b7ede0fb6c1c3de9007e9ff1ecf65b62765b97caebacc31f1803ddaa88ce88878eda6d94a1d52fd6bbc467d06d12ba1d64ef41d9f1c617ac3f89d21ce7fafc39e137a034788b8712a067d13b0f761169fe43a7dfbf9e5b97ca2d0e3dc0a995c8ec73586958ef13dbdcae946507c66c15a7d825ae408a36093d547e2e8c0b7ac24956f38aacf8221a582b06421747d38a357f67067765c08a3934a9a6e0a4c2db1df099e651bfaab8037cd2ccef821db075a95ce2d05d1d1afbb5ce61a561bd2aaebc4b64b80791ee053d184c73aa1adc764fc38a53587de6191d9b2481f728c9459824630c5bda41c76654605568f563b003d90633aab7b62ba765a86f809ee93d64bd4b2f03dd26a11b489e51e1696cdc198bc29db131656569455dc133d2d8c1c5f4ea40bad8e763a03bd573eb52b9cea13b3bb512991d0e6be6c0cca1cd6d4959b64bcc56718a5de232bf9291ee011d19481dc1339e9864253d650de7e6ff3ff49b636e5b97ca65acf924a8dfc2a1f129375895b1db6c3139584c9ee3e4916e0bc9bb51723c8588d5d752960e42c4661327ffafbd2ad24aec5a8ad5f99bfec0b681ea5d83eaff0013c5fa280078da5dc9010ac3500c02d03f5593deffa23bc2044a197d218278cef9f4bfd7a57300309e80945be407d0d0a6ec048e5beeb64d0812ed6a02c6bad8d3a81318d831d22e8f3ab123406966b41dbd2e5c2b2a836e155274edf20dc8cd85cac37f507e483fe220052c0000010000000223122e6377025f0f3cf50019080000000000c4f0112e00000000d50152f4fa1bfdd50930087300000009000200000000000078da6360646060cff9c7c3c0c0e9f94bfa9f17a701500415bc0500706b05550078da6d9203b01d4114447b67eeee8b6ddbb66ddbd6b76d3bb66ddbb69d425c8c6da73fe26cd5a9deb18e1e8b5148fb8c4fa9a8ac58a826205e9ec04f42e16ade84837cc0686334ecd45ec4a89328a33d514812d0c5988cc2aa364aab8298a83b2107fb7b91d5643019426a93d9249a7426ee6494f11cb1c6269497ec6827a33053aa20569f40475b55389af538f727ac326bc3cf6c8a55329638b3ec8540732b56a9cad8218ea86d0aebbb6095f5996dac37c3e16b664a49177982957200dde438aa9bd530d6cc8b42b6fc68cc31f5e53cb2ca69f451453159b74339667add0fcdf45c880a60fb008e0fc158a986411285a1d21883d5093462dd30b1c758e305c6190fbe1d902ccc17986bd3481247f69f888129e3c662a8dac62cc69c87f4e280787d17c52d8d8afa1dcaeb53c8c3ecc73ecd8c57d8c0cc697a208aff49328c77bd1c03cdadcc9ac86a3cc214b98f01dcdf68ab3d06ea2998a20f618cb822cceac0fad518afbe2052da60b87a8616a4898a4490c463b67e80962a0fa6189fe0cdfa40bd841cc418be676fab36ba59f5e1c6bdb4b2057e7b29add03ff9fe643796cb61245987d04536225426a12de71a287331cdf88839b645b0b7b5c2707d94efb00e01fa3882cc66586c35c544631686916ac60c94525761af6ea0913ac4ffcd48340760aa1a0367d29ffb68c8fdd4a107436532568a2b8a4a121a595753493b4720e996721ef62503491fd2807425edd3daffdb96e231bd3407c396ecd37fa15fc96efd06ddfa76977e6d643e20d77e7af53774ea77529ca237ff851e253bf43bc6c76fc7799fd39917c89e9ffefc0dddf99d647790e73b0473ea7000000078da3cc103901c410000c0b36d5beb51ec1463dbb66ddbb66ddbb66ddb7cabf4dd2291a855e1b9a243e292e2ed92f292bb922f52bbb49cb4b774a1f480f4baf4a74c29ab271b23bb23af2c9f2effa9a8a518aef8a36ca2eca91ca75cabbcaafca2aaacfaa12ea29ea7feac019ade9a4d5a87b69576a876874ea64be9faeb76eb55fabafac306a7618c61bbe1a73164ec67bc6ccc34615343530fd33cd351d303539ab997f9a625661963f96a2d673d6c13d94ad8c6db9ed9d2ed6ded3becb98e368e734ea9b387f3b44bed2ae19ae0baeeb6b9abb9a7bbd7b9df79384f2bcf712ff4cef3a6f91cbe63beaffeb90159c014e0031f839260b9e098e0dd50cd50cfd0e1d0fbb0225c31dc377c227c25fc2e9c17714786445e4603d1c5d11fb196f1507c66fc4da25e625fe266b25ef2672a906a905a9a5a9fda99ba9b4aa7bc5492c254176a06b5823a423da4be5079f4307a112361ea31afd95aec2af61ae7e4267097f9243f8adf2e30c26461b9b05b382b3c143e0331b017f483316031d8052e8217201d9aa11f125805b68643e06cb8099e848fe04fa4440154045545edd05034076d4667d063f40bab7000135c0db7c7bdf124bc12efc737f03b9c452c8421154843d2838c234bc85e7285bc26e9f9331534d5307500008ce5960c0001000000ee008f001600540005000100000000000e0000020002240006000178da5d8e0372030014445fed5ea01c75541b833a3686b16d5d2607c9e9b231e69bbbc03649d65859df018230ce57d82738ce5739243bced7e6faeb73f90617b4c7f926c774c7f9215e7afc50a24c872a1952a4a973ce3db7dc49ce315092a4c8935065a2488c6b655fe425e7b8a757b56195a026abd2948f73ad4e89a8ac2ed394140df244a8e21feed5c850a238c4bce68e075ef898327859b8bf5afa20544999776e24ada15c6b5296c5344da82a5125a5699e8c7a098ad448c86eb062e2873fec78e4af86f8b77d2e7c338600000078da6360660083ff590c290c5800002a1f01d10078dadac0a3bd81419b6113a3009336e326414620b95dd0ca40555a8081439b613ba387b59ea20890b99d29c2c34203cc620e7236511503b158e27c6db425412c56331d25497e108bad30d6cd481ac4629f581e63a50c6271d4a507584a81589c21ae666a6053b8ca12bccd64412ceee6143f883a9e9b9bba926d402c5e11011e4e36108bcfc6485d4610c4e27730d19405eb15087084b88a619310373bd0030aaeb599122e808ae5980a40180680e8ab02f60e290e2a21129010091980b9122a85f76204294828b9e54f77a1b59c6130a0d772c5010e131e58d06ab943c160c026bcf04193ec0c26bcb040256387be67e7c90f531c559200010000ffff7a3af26b704d0000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff2","1f8b08000000000000ff00f03b0fc4774f46320001000000003bf00012000000008b4800003b8d0001000000000000000000000000000000000000000000001a641b99401c834a066000864c085409833c110c0a81d67881bf2e0b835e0012817801360224038736042005827407200c82491b687c156c9b86b3db4100a9bfec6236a2828d03120cfbd2a228dba4a7c9feff964065c8ba5dd30ee02a3a0a8d2d973091cb72a829cbc56853ac9368efd584e64e8629ab44d68dd2bda2b8b090d7a684fa62d15dfbf2f0f0f997ee5e17ceb3743ff16d7beaf0101e223834860fcd39cb9dbd0bafe20963dd08ed3fdd0c72336f9fee088d7d92cbc3535d8f7f2eaa7a624f00aee22f7a05d07be3f510cded7e6363f4d890c8818249a480d023a547b6448eaeb101232a65d29b4194a26281a2d06260613550e434f5a514d99fd20b0a0d4d107f0aee7634b6d266492358f47a2c932c131df73d617679fe11461f61065c39f0507c7f5bf908911672ac51404d1049efee5f9739b3e89d9156860051c5ed5d15a0afff17ec5b207b7580114c40b283925d06b04b9fead21394da5d43c2afcfe1d26c248d5ad95aecbdf3fa5a73be34c402e003fa8005c020780006c000f8febff7fa46923fbb374eb18a5d817ca677625cf0cdedd45a7b314c238dc874d1f416cf30ab644da6098f244a56b9bb157bb7006f58d901986900a0f0f736d376df7ee9d0f40f0cdd297cea547441502629dda54b51edbe5d79b5ff0be812ef9de1c8b02793c0043a196485802b14846487ce172243e5a46888ba70e932084d9bb6652cca2a555085e34081710487370414448040ffcbd42cfdafb10097e042dc6ef9207226c81a1819b2a8d04621f001b087d35c8781a43b52c6bb3f2d8369ac1be02c37c1196362efe28b2f4915c4174567b698b12158c529350a416a37ffbbdf4f6b07d16c37c9966a9a4140426022a37dbd77bf6d19ce527f39d30954a1588258bb640186d3f553fffa605849d20231e00e92200124430648962c905cb920f9f241280a415ab48399f01e040203ec0ed81b0408900410605772b4305d79b5992d10bc4b02310408de83e8170c04efeb151d06043101744273dde76ec4308002e43993465b62ed3378b9d8cabf2a24aabffae354a8d7504dd55a2d874ec272533f49ce3c38655227bd33bab632370b9a3f3b8fc24f4c662aa9526ad1b240836ff557aa0ce7b51aaa9faae5f2b5ddda6a7e415441eaab8209121b7658924e3ae5b433644d0172142951a64295ba0b3469d166c294194bd66c10d873e4cc851b0f65ca55aa32818ee1aa6baebb61d294474f9e7dfaf2edc72f132784842524a5a46564952aa027aaad7150c7e3020c4dbc1e6a13820d07131d2f0c7a26993d99f9e9713c0cdae2ed2d9f023f04410225bfda4b93bd31d56bb3caf1bcb7aee2550db5367af6187015aec175b8617bd41608083010a036d65b1c0484200c4948411a3290b5e57a230f0528da4abd568e5ab934bd40cadd1d479c326a82a0daaa81f60df6ce709b4cd21d02d0f4f705dc5fb9f77930b5466d944b8b56674272a764642eb722f9b560c281872eee89e89e4558002e6087b79cd3f0cc2fb8c8f441ff11fee01f74301a7742426353f5c46218f59a0fe0872028a8572f4ce605b472824aa8b23ded350e0242106edcc0a8777ce0872028700e0c38080841b8b49d292e8e14b8e0864428f524200569c840b6b113ba6c18a8806aa84597e01dbc8377124007a3710333389660959f503a878ab960f3fcaeef035ec04b78c5a1bd33c21d6fd644875965db3373f4d2785ff50ad3c6b40f92f986554627da8cb53dbc5a38bb87996e2a9b6a6b261262151bda65c9dd9d7c0335cb6dda7153ee7ac4aa3c1a88290eb075434c00e29681307ecb6a8c0d8e9eb76fc7726b3a7a1b8d7cd1eaa2fe4d07ee5ad797c5fc068ebe4684c6cb1d9656ab973d0748e77c1538ead1effff4e1fd083cdee4e2591edc5175d6f3fe25dce1e2fc7ac902783465ffb4b5efaf8b66fb2685c8eedb4f2cfb97adec9f73354de9b570ae64732bb06216ddb9e497ed76d08b4a3cb4476b427c8a99933d6f2df6ee69ad8df7ae66dc302db4d74776ae79a02de952bf23c782a386f62bd3b7dcd295a84d00000d5d4173956b0ee34a4a47f943030b57b4e8b6bbdf3a8535bfd744cb457595bf12e56f61cbed1e1bce1e4aeffbe7ba33debd2c62a251d3fb0ef9cd6f17d1073dcaa182f2722147b17de3003ad2a79755ffaff53d525d74b2f605804f467445c78525b534ef8fb8a1e23fade228b2e9c83a61f39657365b436a590191a69132d53e9795d1f7adb6cdb680b3f22827ebdc83d6f5ecd3ad3dded76f7c0517f361fb8dc67df34dfe3215038ed62d7775c4a41acba14c76004c3610f49ec2604ac3493a8dc9394a58a8d22a630d181160022acccc5184355ba2eca90467c7e35cb93b81ec7852996aa7d41697316193ac3dafd84d63a22e6094cd123ec247844048220a2345059ca4dc4e51c8698a204359c95236729497228528511eaa94831a65a54e055da0fc34289a2665a745b918502443cacc881d73ab1a0a984c1b4c08a070b68cd9c56a5860914031dc280b77ca89cc99a778069f0a70a1a99ec1a71a54482d1532e108a1bb0166b267dca6cc42b8e52ec8b207583d548eec1e5983f4d4f1986736a1ec253279ee15387ae52c849b701b3cb8c17831acc6c391cc80394b144686f251a430ca14e63ce344ac1a2b6121c708967011368246ac4c13908bc42194089c30197076a01007cace892110db361441718e798701534014a6301152a2142f86849bf0106ec263f01807e70582e430f4088cb01036606ed904e126c265acffda5b082f6aeeb2fb0aec959f114b215e1ee6cb6e6d800341a6fa36b67180a18dee100d734b336aa16b4f6d2ccd0d5b1b9c5eec2852a48fcc0491d92c1c60178ebcebfdc34381d4ffbb331b3877dfc04b5e4025221a531bdf3f269408acb44c000a4c7e2a0102eed527483150e3b5e2a4c055845ad70400c575de382c8fb74d282fdc5c0b18f3fc4d0b93766aa5955b99ab350501aa0589bc5d8f85b4c1413023fd01db4c189886fd44e08d2e5bacea5e310dacba74ead0ad47af3efd06f2b50404d7f4a04451a2d15c3164d8885130101e2280ea27453104305e36148138eaadb3becbffe7e5b065c3638013f5bdc24ab8c60d2c8d800e70ccbe48f01fb8fb529b00077a35405f32687066341c58f0c3dd52df04d65ec73d31c5750117c161c20fd843aa66fb57efa405f7bc74e8086f82900bf92c37b63d9c204e14278193c2a9e3f0b8685cbfa2c770748414e0788be3334f79a7cbaac16ab37e81e3c7096f5aad74f4bd94ee66d437cd72f4f3089fd7bd73eaffcd7fa3ff4fbf4995c8bfd86fdef7f3cb56110848f4330025705f78489dbd9e39c3849b33d775bf23cbb27f3a83694f8e47200423a8037933da3b7c24df6b7905452527de6b04008026ef9b3ea90ac28103fc71004825bd66cdc6b45c3f007cb08127e32e9b146eca3c7a0b322cba67b2fb263cf0d84d4facc9f4d4a6655bb665dbf1c2a35ecaf5caa1a77d94e793eff6fcf013c92fffbcee3f7247a2bd0fda00caa17d075010ec028a823d0035d80bb0c0e77cbbdff6e7b9df1ebc4379feb787ef485ed4f6185edaf6045ef6edc5bb8497b7bd9257b67d28af6afb0e5edd31fbb60a5f7d5ee38c06e9c8e2adf065c1591e4ff69452c335438acc01f6dff1c4937570258ead2fc1d385d0838667f23b169db8e714cef9534880f738771dda2e56b7bc12d0b36e351147a7b245aeec02bfc3eda569716e8b3308c4d3c2f3745a9e7b4f9c395a9a56e7b686b492bcdac6f1b8138ebc255fcb97bc35c40578f9d25af24daede293ff2ad647134845b06e2686c6f294ebbd449288df773725297a6b50ad201f2add0840b2a4c8264dec43f69da38779ddc257543476b47da63f042b44bf13a97b838ce80f6991b36689fb9f22e6d25f64ffbe434222590df526109cc33ecd948bb6f62dd977227a7a73d2dd37a5c9cf6183b4e887cacd3c3e67ce6682702c49bd169a595e4552e9c7ec3bdeef413138aa4b3e4024e42871c7869da2ee7ae437034c03f54883b8ddff5dc7b4e32d0c26e70f20cbb9f7bef8910694bee3d48faa58089cf473680745347ba53f5fa814d21dab10447da96f41134d846075bf738f79e4b9733e280f32daf9c0cf08789c269f7955619fa26a2c0d6a6fee21f22f18ba671a7f6781d70c00900d4af807c2070ee5b2080e76acf4310e0a04cd81bc0185cbf44da1682079e490756d8968500a6bd681f29b20b2d970db282c2d68029ed7d136c5dba72fcf01702cf801c4101155ca2195036050ded2b059dd46508d92fc60a9527ddfbf1809af311ef83770117ed789e47a7d16008b2dd2dce9597ad31ad9788b2d317af58547a72c699d56172229a3c585dfba87755c8c269a35049c4c5443b275babbab7396a6f3cce7972c9261bf63a93de25d361d7a23e1ec7d8207e24adc718472362aa353aebb0d9b1b0264a1f3a93986bb50efaeeceaa39a031b5d2c962eb0e4e69635b7db5f683947885353667bc696430bbc5b174b2b4a6702964c1d6204e209bd3256fdd99a2579f50c441023223d653531ad4e6de73f082654c42bb6dd92a6ed43250904e08774312ace718136290618680124c298418fdf310134305842d376ff3704624219c21a84529c5e85e07108765768775bec797dae5a8657c578bdbee705342983385861a2dec326497740e294df4326dfcb8b9a42856cc8214524effe1ef24e2c8b8396da8edc999a2e6eae15a4cf7955e883626fc509b193b90bee9b5b83170bb4dd56a68b1c12a50d142bbc84d7d4e460cfd47663cc320ee8d4bc2b24d29e52ddc119cb1a62e2fac0a565fa771a8d6c533bcb66cf52ce9004a92a9b932748405bec33567144f4576135e42f19e6a12062a50b18ec2815d7ea0d371a0de9815d8d49452ac7976dcf5502a3f234039543933641077b283f5f4d907ca021298ce655c919254ff2fe853165c02458c47974e4af23822ab1c44a5375780ed54979017a49f1183551cef9d8208055256d23dc3790c21c0c962e1376e572fc86459e596467845734aa9aa5d1a62e260b3bb9e2397e72863a5b17d95b37fb2e6e7dddf2ab7d861fb48fcc759fc20c8d16dbebafdcb9dbb6e9ea17048afd827586c2fb6d1b464fbce9dc6be8d4e014bf218ed668a98a8289876698e29a50031d38405ef66fcc08598feada225ce0462c2b5d44b0e2ccb0ad40bcedd4389fc11b4a541e34ed69610386c644ba3f5e5a285bd1095c5e2b8e494d9a648849770562b7588f15521bee4f3c59006ba9b5942fb63ca9d80a28db7dc72664b0e4dacb1c4d48c2739f3f1a077386d893284f126dd4e6d0cc4317251d0d2fcb9523dd5cf656c7bc2f896684f03d516d02b37ccaf6479b6fb1a8c27e0bac780e88d159258b826953ca58ea5195de5caee52583d231c50c413d7088727cc84b4dacc2d1e6f6ed17493bc29fbab424189ca110f56601529332b08850c96d85ed8a4d8a1852cf10df2788b423a25d198573ed1e13a2ac99cf2248eba855881700cd2ae0f49cc98f270c7e29c151aa5c4c8d999cbcc72b2611028722a006c459d44842ea5cc2195e2e11d50482b26faa9ff92560378bdce2dd7b8f9660a958373cc5542ae412d7e755951270355165e53f1116dda33d49a61f86cb268d89b2c51f904ae475548d8a61ab7ea47fcd276631ac02f8be935c1c3319f844f62c54c78b845f46a75170dd9d1a58824f82e71bb5ca15a4eaa8170ade346471a20f684abb555b9ea3b28a1c2617762c9753e18a80ba223962372d98c5220d02becfbb74be98e4602042ccee2a4c0402505612f546b4299908b82556b994b3192b42a2fd180505c5ef14608435e9b4a55ad46c5440aeed73faaf178468c89b841ad004a1d57e0dc1c2326f59859715159456c10cb330b1c28a3736b9bc6446b16dc51cd0cb91037406a690e5c2295125cd91f3f3b7e67b6c04e6022f67869541aa97d5b942326a2ef4ba1d2ebe8b9b1eca855e4af89414dc82d2c9a9b3638d9aa2a6d1048b4a8549f04a6ba4809d12846b3ace7c098e54d679c0d160c4b3a1eaf7dcacd2a57aa546689e55a1ac0da10c3b15746bdae269d11e70881d9e99e27ffc69f28c52721e71bbcab2864c18be801cd054fc7ec9fbde41126fc5f6aaba74ec47f7bf10368af46f85ff434549120f76805f24266e17e2c976d0d6c07b3d229163271abbf80d68e5e81aaddbac48252ac0fe88bed34d79a17c904923a3a530a95fbdc203e488c06716c43dafc3a4e1a5eb3dbe07b83d164650b677bb4a1d8a54903e4530d494a334448a62fdc2c0512a74c6902dbd176d8191d84ed5b6499962af2a78d476585033ec5f4231c43f9f92ea26ed88e708e1bf69eacad8aa1d2a82cb7ea8d1e68c59a0ed4379cc83ed19e8ccd68e20e476a3cec2930940462f614ef05ee9a0c624586b93815266a30eb33025258ab4449b60a24cd929d8cb2a9e63806842bad7bd2fcac8491ead733dbc25d35bfb41938e66e0b69814aed29e93dcfcb173a84a4ab448a9de4c006f1a422accc1b80e4f5996f096efb4697ea071c18fce77deaaf7c60e7e5728edb52e5acbba663c5fba5abee48d4cd9f8b6a2dd9e696b0950dc9b2055d87ba98885819dae7817ac90932ad369593ff443bab32c4ddee294dcba99ddf1c405a11b6b9f1485662de3d46e5b823e96b7eb93ed57d16f99c59caddae442a3c32e6dc3b87534fedbdcced661a7da7bc118d71c23282214dc6468b2b9695a14cab77928f81a32c8515e594d29220b2dac0857b35600e32f2a3ca39e5c3e2181643eaebb2db452f2755adca1ad558cca52245f3b1b02a4ebdf4d8b5c67361f8f46078313da4311818f68749a3bd7d171808082dedf1908bf7b4db3658ea586514ad71e4997fef6638399ed848e13d5071b3fb9be459e8f871196c3778bd4ff772be062f9c9156aa436c642031271a5c368f87ed8563b83890cea6cd8c07bb8cf2c8d3e850c18846be3e92f77c07e5af5e8aa375285757b68fbff27f83ffdfbd1d37deb56e14cfb53e7950b0eb1beb7de9c193275f6d4527a78fac28c18da333fd6a80f7051c79c994027f6b509635d443aec37c16bc178c2262dd4455024a281e7e87f2eaf9d1bf9bffc7b2a6b8410285f742952867c4b9a6a7542bace8ccdc6f69bff1a013bb5eb1d1c5cb787b77a230a78318979218e69f1d2fe7dd1b6bd510d4b374770c33593e49473f7cb940a79454e46655a40422cbabb33a7bb272da7b4946d99d9d19993d9d947ca48583afa991bd979d89859d5fb99d9fcdf161d9acce6e645717c58818537e25c2f20790b19db79da9a5bd8f8991a3b7edcfcf01bae7a28901bac9beb981d183edd19e31ba54a3ced9abdd95b397731c1dcbc3ac832cf433f5339aba8ea2ae7b3fb73c8926ba0bf84babd0ee63d3b28470ef6ee6cec4ccd8ea42d8f1179e31d4181b7d59797b2d7d1f47331b8fb03caed5129ef00cb05581ceb6d88b6f3a24a41b48269484336ea384816d6681906d4fb4876444b649fcc05b028b0a7e3bbafaa35396b154444d0ce32324837be810502fe26529159165fcc12eb68e85a7ed89b49374a57438caa186c5f554a8749582f33c67921b5d6c983fd91977fa3947e560e51dc69df807500f4328473cc0c82d134eee1a2df1f3bec5cf2efabaea3586ea2947250ecac90c100b8569c94d5248e946ac4e6ba8c1e6857707db4a70851fba9be7dfbfdd3c07539b607f9bf5985a94b10a6d9e7a47daa1e4673f823dbff094f1209ad4cdbd20a2aec8c8f9211b9e6adb6847cc2f892257d717d06fc17e3cbdfd76e5ca13ee53399deb96fc3e0a944ca82287088e822252247de0effeecf2c1d3198fc42b8167e3243307d3e68ed2b560738c1155919f9a5b48c94aabcb2b852849aa5e074b554b360c9baad053b39fe64f7e599c5f7230bdfbd464f54d9ff9d982202dd2e2017267f733ecd9f49755f4b5417432353536a374b0aeb491969d805762f655fa76a541e209fce847b143cc5abcb1c7b6b2fb9563f9015ce79f51a97db70dd303aa4753a4a81d7ea4653d21a568bca2b278c25acfce5872d47846b2829fa77a4d516b4e2994d8e3251b29977d35eefaffedfb0fffee5c8b4dbf1e29e7291bde1332f1f6c9f4d4bb272c511e89b535bf2bab7e96166665961571fb0a86e6e646876793b6fcb74d3ec557712f5d0ddf4cf04c6524f9ed904ac30e6e2791d67eeaf1a9f458e6da309da83fb90ffff692fb975b7de47182e958c3f384c432464d45d1940b1acd9c15f9774e513ca954513c718e57b34d46af454f47af59efa4fc10b91c7643d92bb154d92b698ad7b7068a95cf372364e612dc4303436303fd93825de850cbdec2d8f5874b57065b7a42828dadaaa7341aef8f204248cb77b64f4ff846af2a7529db926d4c8b4a5e61ee70b6c7c36e94055aa79db1d4523c8d79591649d9460c63b20dab2c6e9d5f188e63fdc6f6831a2235f5c079e4ecd009e2e33bf281bf2fed0c75b209b26136c7923e1cfef2d91ee8d0d98de028b4aa72d408f33bee8e92fe9eac1353fd3e44e341b237296941629ed62db14a9b1f1238fcd16ddb0310b48df71f66a7fca3ea7dcec4e04813e973ffb65fab76b36be6d692139945b56b24522b5342e22921546147f38c65c197a2bf3fc05422550a6e172bb2b40fbee683f1ee4ff30f74d6554edce7fa53f87e9e6fa8b9b75868d8486d8eba1bc7bb1a95c14db765fd3895ea9885389431314b5bdfb147763f5f6356a10b5ab09638a6cd860680a095353e5a89b3d698c9f2c99a02085a6ee3c2671f3df3a949934054006d08db59a9082da5992804f2ee1f7b2dba3aa58c93985ee0042aeedb366e32a445d6d8c8ba1e1d62950ea65bb65aa600823645dc224e5f8afbbc972272a3b469e607bfdf3e78fa69f3e6540a899219575a01726505dbaf4cbc3b5cf27eeebdd71a99526dd22fa303eb871aa2ca99a64f0b1ea389ccb51fd82f56f83cb78fc87e5d99c84a35c6f37c01e6545697f48ab7f7b4e05a95fe7ae9d2a4995569cf63aa167317ad9fa5bb5415ba1bf8b04497b5b77f90b59baab37a0382dfe3b93a769f07f62df8cf43513ae33627ecef850035754d6f1a02879c5f6eada0f67fd8d73bd5b615e767b6d515c17c2dad303d8f72e4e2ede0b733e315be18715b291d4e63edd1b3ad4e02e80eef8d5168ff7d5f2c9a7759de30b337b7ea87ccde77d4f66352e9ad4c79bb40f6e5928393d4eeb2e4bc666a1590f2b1237d37dc59aeeec7ec6c54be894abf9d309ddd555953d4981cee549918e47727d1bb2dfc0a76e555259c58b098369d3d53dd56dd92f4b822dad72406b30210984980c03c6af49735eda06d81bb00b1ce257e280441bb3b379e350e5c6e69eb1f685cd724ff1764baf2507c9ed62b3e4a7ba0bab7243479aa1f03ffd9ca5df6f3e4a0e82ce7f1fd69de63b441cc9c246f186f18365a729017419b723aa59fe92abd77fbc1e3cd7dbff397ce3f7fb279e7c16eb0ef4ad2b367623e25625362f1cfc60d79d57add8613c5f2fb99dc8d3f0a0d71a3af27019234599d009827124ec072683429a4340d0b2ad31301f34462f5344092a64f0ab65fbdbedb7fe671dae9c77d27d5d2abcaabca8545da9f354192dbc74f1db1dc283d67b8f9ce08a507300e46ecbca288913e393e460c6b62e59c43ecd5470c78dbe70a4907f4d702873a2709a3ad12db458728af643d18f7cfe7e7a6ee8617a3927b01127cd10c2793774ec32798ae97dccd1fb7ec59b1ed19c929bbebaf99e95316c7ec34c8e41ad335e713b6c92405c41aa3beade5958f7d7cca3c30c81c50941b5858bb3475fc6acdad8bee92bfdeca4cde08cb50a20324788d8a0acb5bb8f64ef1e70472a2ea5e2ecdaae1934d753fa9ea81bf1976038bfb2cd192f877a328e3d1c131b8f926bc69688eec9d67a9a01be190771f13b51451d5ca5d2acac95c90571bf2aaaf8fda177e997ebd576787a5e2b5b2b3578037f115f5559f9f87e4bf8333d30b6114545c0feee1cf4f0f63b67dd4ed3dc2624bfbf32aabc86e61695337dec8d33f8f57dccf1bb46efc605bdb9b57f5209787309e5839ef00cbbc4dbf03a203c2c39df33c24cde17480045f4cede8eaf44551e9d09014e7a490606d9e11f57155214df9f21f4a5a3a7a84ab18d17361ca2afb135d2ed2bf46c5274b146ff18eb5f2d96d9f5d8d791a63b7f59120826d57678a8ba52c3db71e5036eff788f545c4afd297a203892997f212592f2d33e6dfbd1ad3abe5a5abc76347d41179497537adf91756ddd659aa29f9df563e75132dff9fa331fef70ea574032dff978e1ae97c5a50d8b1b138d4bd5190dff5d440778ffbc7aef4f47c44320b1d209ef5a0e22228b766f7249113f0f1ba47a46e4239c2a6ac2fa7fe6184bbe4d1c1a9f9a9b074e6905e807846670a8f63ccbd96fb3bc1cca87f94d3615f8271286acd6d58093d1d555e293fa6530be8ea23b064e7e22e95eb4672e26217236cf82fb4132ca2ac7276c60727f6af9733ccd243bdfd221d8cf403f57934fbec6ce21d4a5e5da6672eba44b47bd2995cc921c4c0545b5ffe181de41275a949575947179754698c35484ea92037cd53e7915e0ae9c1c1a90f06466e82b929fad5ef37570c1ab074759aedc7dfe859edebfc78f542fe6cf5ebaaf8e623bb2ed0b123ea95e82825345ca912dbae3ec14138c3357ba6c2f3fbe543f29a176f88c4e35fe5bfca9f844878f13e26bce7e49a3dbb20a4b4ebcbb1218167afc4ba075c14f73d15ca7f4266c954ad32a1ca8524491030ab51ca328c0ba5c90fed5f5bfcb252b2f2f93e34767967484192785f4f70e979a763a7d38b6518e6aeae823e470540dea8c0627e2fe92059365e8d1046ac0f6e0bded79324d2e4afec808987872b252b5f96ae8dec5e9657b0f8c2d2c858cb374e53ea2e33397ec68210e4de0c358fdfa59851b4079b7f4d9bb94779e07bfabac10acf0c7b8559d0bd41144d327dcbe2991d071129f337a90207f43743e28b421bc7576e09d3707bf4ed0adec947888afaf728acbd653b15cabc5eabe9bc09ea510de6f1f29990078a79a3a3a119055ffcfafee68dcd475726835393c203336a525070ed8634fcc788c6ac07f71a3372f23272c90597ea9696c35b4b2b33f24a2b1ae266e71ae3cbabb2489535a1adb7e7fc1a8ba85979eab0a424e2a3d984bdd9c4888749c72fc1b9f888c8d938e2235df16101c5cad5b07b88147c073e19af37ada795acd5a195c21c766f2db294838fc17793f19dc16f5432789d6e8b9733caf3ed862fec47dc3c6fa04a8eb84b8732872beb491dd19e4e8501f735476527c35c6bc319a30eba728679be9dac8b5bafe1bb77e9295db594a266a2b7c179cb5c1d51cb6ea3084fb574d15e273e53414b8ca9af81a1b265b6ae9035cd1c4bf0357024876423ed5cc631e8cdbd06cef8f5bf0aa2069fed6afa9b1b1bae3d1c0ba3dd7858d7dcd75651eae36eebe21e9c9f14191f9cefeeea4af0fb9fd5df7402d3a1c3ddd174a2bfb7411cd3d9d28c6d6f3c4eec596238992ea49b2c309c96e6871d4cef2fcc9b2e8f3ad4044e5db1bb68971e97917429bc33ae3f44292fd9c04259b199ed525f467e4e4d51ae869780954be25987938ca2081be0a8a5e81e7e61378e81ca69d116725ad318262e58706dcd2dd8ee7af1febe7a1c36fe1134f9d5a6d09aa7a63b3325294d27411ed468da23d0258fe8382e0f227c30adde0872d0553131d7f0250f28bce06da6360ba04e4c0594d7050594d60606955604045654fc3fa66262a8a66e62aa72ded422c4d444fe54997d48554863576346554643170fcf17fd6a7de3de5a8790b302217e06f27f352e1a5e546b207696a16a52c3028282081683c5547b74b9bfada991869cf481a19aa9c2e906efd6c4e4fcbaf880f306cebafce5a7a8413dc9a4ec8e94e046698b0bfa86c6171423eb20c702ec02d86193ac387e4c493d01b4fad7452f6a56e118ea5d28a26b6e6f9995f16a1bd61beb5c94ed4bd4bfc7ab2cff5f7200473b5bba3623790ccb5e2616f1890c570b967a354550914196f68ab2e77e79a1c472f43ed436383d514df78ec6e832b7fb345e9da9adbb3ad569dac49856ba3eddd18af0f58cb425b813fd2c7c3c2309d61ea190a71e1a8dbed805024fa57ac48c4fb17af3844611437d93235ce850f3ceedf1b9d59bc3034d97432274cd4ba7340814943e6756a6534c0488e2f454bae3b9771e411baf1ec63eb6a8323e8699b6e38ebf29ba91e4686c626964119b686c611ca5ab94491430b5dafb55475ed7d89769ed595b447d797d32803045eaf5e3b264fff6c2c6ff240624a9a9e3f3d5abe0b83ec92fac73245654de8a63ec7ca60421d2593b613af6b33380174193e42b5b0f889d4e9d617ef91ca3c600081afd9ac4b1a2a6caf8a4a6ca3ce084a73bdc5f7ec318991b6a4a495b32503475bf4ab5cb336b5aba32663d72d6e54c2d57ee59e5e008371b25138f6ba5f6f9d64ee4655bf74b418ecf24832d76781079c33a76bb6e855d57fbab1af60636b7ed824313030229a1d6e318eaa791ab518336f2365a1603f6810f5f8183a835e4e0c08d8af2f88b1fb4f31ae2b5232a8d1b4c8ebddf8f8628a104f3405765b85254792f9a2c28969d53baa46870ec27738d5b63f4df27d4dcbcf5f4f8eddcb2f0d7cb19196df191b23305054fcb8cbd2733b39cc7e4541b3f285f265f4b37f6416671f0b1938d5d4b5a6e741574cea6a754ef4ed8eb3d747c39f1d2438d149d03c442d8acf5f3f032b188e02cd594f877874c0d733b6d5c96ec7767a646a731e9df36a938c04ec2a43cd967e94ef984ca8c6798fde8b631b9eba9dfc8987d186130408fd2f98ceea7471c4e2f2973335d25226b86df537f3b863a4324a218585de121b6feaba07082d7cafd64f4bfcad6e41eace17ce1975c431cd3a6918e50a41f47510867c8c0f43eb4acf668e64da5a75473fcbf67a4a2fc9bd11a3e3a7eb3c4887b07ffde462f237bdae672afc5059ba490b2bee5dd09d7f2d1f45e88c8dd6096fd3f811a6f7a42c96f7abc3cd6bc46ce6b59d5bbffd8a7a2f5583ea7600aa9a246cef7036b4d454f794f0dfb37ae082589987487a46817d7c498fec48489c404533b593d7d79397d5d6f3dbcbc82aec119c5171b218939aea7873d35fb854f37ca272183e3296ec7a33cd4c4c5ceaaea4beba05f62038d5c5361a5a489687fcf45719672de0023d70c18b5783c13f8782e88170806183ba7c24a62e9be7edeb7c40ace0518399360d5747a7b88f7c30dedcc43c8680ff6bc2f8f6a6372080b7775890c719374080d75770b0b73e41391840a0aa5b2b052d98568cea8d1b31c662d820aa122746be8a55fb848a7b5cd8255c688a4c367861f7444fb89639be244e27f0a9f40b00d250fe50fa50cb1208445bf04e58a625b4224c2da694b88ab6929e9f9e914ad218a7e95bef0d2f87195b88e2edfba1ed1ab215a972c1f19171014ecbe49ddec3361a8337214113e4958b39b0f83c7948c7ee48b1c0bfa27c7aff89dd1008835bafbf873a9626502312630c47d8bbaf5eaa551dc574a57a737154855ba143089850e4b485f37eb3b6cd9de2556a9c7b7560d1452eafa4053515bed00a5b0aa0faaaf474e9173e0c3c333946cf888316aa94bfe3f65b4e599e0cce09e869e38845f3ca225e9605b9ccd343c53be642bdbed588f8eb0697a112921b9a93837a13305e15bc7e29da16b648ad732373335e9f595c812cde26683789f19991163738be3a21b6bb2299d1d558af7379fdf614a2951343bcb25616b63abad696e916c78d1ba9de1cae8742d9dcf0c5624ebc5c6aa90d5b3829ae2d14df1417a269ebf6355622f55c6a4a456c690e695ccd5b4b44dd594942faaa55e54a31c9edf97937915f1caf57bca376dbc8e5f6c417e6a426b53467e5d3bac4cb3be00eb995699e595d8e07eda50836061a971e122415fd7c4dc0d8b4563d102d8c5daa9aaa9b4397fc3a99abfff6e7570839f88cd9dddbf813cbb101fdfb3ca533fd2ef2ccdb77b025230ff00d002b3fea8282793b01d1348e8ddc17670dd7f7eccbac0afff335abf0730eb230898323bd7f5acd6851b821f2a163df918c85678a7e4098f65a9aea344aba3cad6530ac0b2bac9671d43cd3f65f64d1df8e87a02f0bfbb8e495ab601b6b2d007aaca776f290fed8c23a601381126e8c292b71c815af400f9f2045df0126b8fb5d7da67edb70e8897872412b7445cd2aac9f8900b95c83926269d8bdcdab8d87ca81f39c8affd366a5885ae7052604b8887eb0f727a32de9fd003e798203b1729af0d3247a49df16b97730c65e7a255a736688df02ad848fe78155da336a16a55ccdc65838abc45a0af42c3a2180ea1fd5e350da3cf864ae41c130ce72273b541be7a35850889184cc65b0560504cf89e682aba7f52537c6f2697aecc46711bd5376c5e238c89b6d584ce0356139f6d0635fb6e7f8fda481150264ff867aed037e63005dd0349f5501bb4da7b6c6c13efe4251824f7ac04e26e21f07b7ea9e29bba6b6d906aaf933459b43a117b8b93ef8e0c2548b1d74d8408113723e86660d7af146c5cc05d8e01d0ff1fbd5e95fe1fe049ebb7086dbcd33b5bc42e35f65755be355caabae26b941d309e3b065829f82a84e7fefdb8aa2d295d58124a54e5b8b5b2025a3eaf4e2c8de6cddc09c00bd66fbdec0195accbab83c62a0bb3614ce4504d07b8e85d007bb36f8c5197a0032cf5a619d3244bb2825e2a9229961cf921c2c0386251b9b24834cd3ff0d22911b783efb531a707e8ab173d88655e3f201e9b0bb440de0895eb015c0dabe1db2f1dc80b666c9663c4aeef06cb7211588e1b3cffaf6028ba77519969fec105f4c7b5107a2aeacd04d077bcb3cce79d2e5c77f0f5ae2db8e1587c9900a4d91ceb0210f718475e848abcb0b26fc369f0f1dc578e390540fd7abce38253d14a1340dff16ec03cd250d4c60472cfc6bb73f1abf0680a48ba70723c1a8d5fe458202f4410bd5c2f6470714b14b9e9ecf3ff1b4005843690e8ad08fcd522894ab2a9bfb92de2c78bb24338a4df68fb7ec0c2fe5e0b671fffedace5dfc98817cba22f6280962aff1b2029c6584a94c0351c5296be39f5f5801efd0e53adf25d6b4d581a4c5947e192262f5ec82e5a58e5a4d653204b79aac897c7509d027cbb75643e19e886c6313b96a9e9aaf50269969348a57b5d68f58ccd64f7fd89236b9108a76eddbad92c05acc1b06163cfa466fa220367b65739ddedf774ebd37474c2a52bd9b7e9a4ad97dd3ed61e7fc107f8620e1ff3bfe00c3162eca768b3d6d9e671f200b7c5f2d5b0d5a94947bbdbd98eede2f076c774dd89df47237bd127a00ff3117bca7ad92444dd71f6d100e1bb3b49600d5f27d100513a91e6bcee70cfb9600a6c3166f2f5694635e4553c904ba9348508013293aa0b2abe2f41cdddaa30870de3c014fb26869de9e90ab4023e856be1b2e25f2f8419d734357e732ba48ea9e246fa9e90c478416431b1f5975992d8c5540d87a4e01b1e1dba5da0357d7f00f0f60730f80e3af2f44c5ff768e4b9ec3dc22c02bf39755817bb70549d4c2375d612563b6086cd1a52b80502d3d5d994157930295fe110afe10600d220f0f0bde1d38dbd31b2c58b12eebfd13c9b73ff29236f952352b736e9033a03413ba9935531c91319a71f04e4f3c559eced804a558e75cc2db317a31fbc831f3b63649aa5a38789fbccecace55ba8d8c72ec1362c8aaca06f58b7432c07f37cb8605350c1ab6c2b52cd2849d07bef7b70e2acbc431c20798cc9cce906c224131014f4471617e33014ac6a9d1c6c9386317f9920076369a8fc15fb505e0174b7a1e3e5e60b1501cf3bcf5fafd8bf12873f217a0dab03b183496f2bb7bd85b734f6f18f1bd00398aaa6f4f58685027f618b08a14453d98a14b3ba024c485324ec9846313576cc551b4308ec5426820d4b0d1818db598a5fc4b27345b1bb8c01c4fbef1a376739eecc14df3a445c8089b13690b5610632b6ecc556c4c2dce67da3c975d6275330e47d5b8971b389fde219e973febf294871b6f5a0ff9efd7e3a9e862edd12002099920b30fb0f6bf0bf0ea41ffad74546be699847b1ffbb9e637a110e1fc308090ece4d94f9e485f10f72ba546d58653590ff07bf47a6c159904464780aafe12be4e057dbb4239cfcf6bf7da252bfd5edebfef5d87aae879efba8c0848a690c8fc38d58b24f02810b60fb364ca931f201868daf5b5b2e2b31704121282aa7300f421d919bebc875cb3e553d5b1f8512585abd199c6b413aa2bed96c649858392765d47a9f54bd43d6c8a094322cef9533dd67b02caf0786c748fcc46ad883577e5fea90b455c8836faf35ec0f0226e3528b9184afc1983019489953aa474e96cff7fefdf4de964ac2a0fb68016e6432e2e5ee342477d0516d4edc253196d52c070109489893230d5aa50b0e4ba19b420727f580e8a394374db92d1ee04376c66d1a361ec86dd925b83ff3ddcaed213c878f6081f8788d7c4fbcf2c50e53a2999ba43d41b64b47d9ad1c461aa93606152569f187d32ea37014c1c5f8df8155294439fc9892e02945614fa789c92c14cd0302c984c65b57b9882e0060bd8167899a112f8d090755cf4d91366541fb419ac353877f376b353234739b24208082261d8eb00f8c0bbd57a5c7b0925784f642b66deb8e3453fef37216133205ec2c25592d0e6f7678ae4d1b536bdc0515e5a6059d665beff504cb09901cc39c5f46cca7ac403491d1ed783556db590817be4ae497c4f970c5d42d537728dadb9a1a4b019f23124d02d07d4e1fa37aa4cffe7f26088d4a2912abe551858067c7c1c2b58045f1889045b3b6b9d31102e0966739a3222b314d1fb787231db1740a51866808d865db737238609e9ae838ec14f94c79d50fac7e9189a5ed476dc264315842a220f0b11e10264298c15a5a3654b468b5d9359c8108dd02141ca6d99b8a709b9a345c5252105677ea095f7487e318e2dba8e0e4c2532c849cbc6fffa999cb9e33f570fac95fa1f4c39f02fb47bf05c8f0aff32207253720e090facae9ae079487cb3718fc605f9a79e5f9032b999db256bdbc156ca84d4019e42c64bd105859daad76930de67ba908400acd5c1a32617bb75de00515539da0a1cbba6bd19e9daef21aeac2c811c6a70fa669505062284548f501ce34369d893085926aa161b1d696a8155da9fc181d864966c852e43db049c615be40f411e6e7a6639f0e3496afba3919500ee430d94781a2306f34c813b24b2eb3228e3449629465b9f2a5c6167893925e581f916a09ad00ca043cbab6d439ed735f06bcd5de468a04f604ac9d5d816a9b3fc014795ba514d65d87a4ca31e240866f3ccc0db8e3a0588e39eb1d2c375ee66932900ce9d95dff69d097b38b51f6751e02e4f47e0e4bc6b0306b370b1fce10df0e2740965a93555364e8d8b9e661d4a3b3b3181d38516425f43118a996b8ed1cab565c13a04231d145b332630a4d9e0dabb5835f295284aa9410a966a8f4d97fd5563d486e74a45798bcecb2f4aceb6b17f07503357c3b1d8c7c96d18a2c85721f72533335e46939bed8b4a6c7e76c814bbb83461825090022d437a5d4be9f01863199726c1c1886622d75f59e3ce57a4eab9ab8c335499c32cf3ca48c58f6f74a0f04c42f826b3d5edfb05dfcd05ed8581f2132b66c5b21aff3d99cc59afdb70a6b3af8caa566ba22ca5b32cfeb263d5b5f1e6eba71dd42be4b2d4930220b51ce1aeabc1d449f89ddce03ecb7dfb6a39d582f1eba701b8f871f59868bbadcc5ae5fe6917f518a66531160ce1e3f90b910ca6e5b8752f770466e67827a6a06537edd85bdda15e8b1ba5bc45fde1d8d144af8a278ce759603bb194754bbd48e9299961d51b5269873b36ab47634b2e56132f82c1123282cba5e98cbfd2fba8cacfcaf15a5cd3d1757502345d5cf1a765babeb875670cc11d2f10258f1eec37c2c762b7df77d7e680807aff1c3e95e1867cef38737acac27d1136624cb7fe69d35b1be5f215b0f2c3bfaa41373506d9115b2a95f979356153a54a7e37d516269f2e6c964dda296a24e2a4d446804c07e18ece19aaa5fcd664c618d220a64cf2e07fd2fc2ba3fa628e7c0d4e0c0a8f9373ffe4b69a860c0df8a6311d5a2ae4ae7c2284c4ade1ad3ccf8d2b2b53267a2c697eb577a928248917a8fb3f7c48f0ac0faf4f8cc1896cba8cf0e21153b3aa459a9e7cdc82a586acd60ece7d3aa0e971e1f8228b5daddacace4612475d4657f776ecd76576e20c3d85f910a7fa80b20de796e6ec78807dfadbaf0265811ed3048a0312cd825d696d8bcb5195738fe9a914c7ec036b2ddc2caa47b8f43ed3046173a49633c2de1374a0f02bcf46e3ec05c127f4f874f6370d9c270d6fe21324f4144b07ee1047c58d53c6c170113fb9cdcd36e194d5d7335203785b7a7d1140f434ac7dd177149d2962b0c3d0d83476719a22bff9f77923937d769dd7335677b60374303a5c85446a52b42ebc4fc6e0d97488e2dc5ee581365b9e8b59b54c57c4096d8da4376081184ea951775cf426dd0a65b7b98e0061550ab1fb7feb06f231e39fb23adfb5fa347f93ff5d77c1ba6f5532a0c99723a4511e3dce4e372489fee7f9d5093f159ae2763fa35cd6fd380df0efef6ff75860f0a615f6f9faf4677126b469e6437bb08dcc8930b929f73968096d145b87f771c35e6457df62cca315592374cb115a307df626626bb6830c78e38f82809203992de1caecf58365bfbe3da30754be211552c6d4b559aebaa3f04963574df9b6e3ca0aeba4aba3dcd65313ffaa45e965c1292269b03e6c1bc5e965249cb16ae2ccd52909c5cb5a57b0fb85491e3abfbc7bd8f4c45b2d6b5c7d149fe4f7de6bc8b1b9d1eb405de37b29cf89787cfd30bf49715dece0f9465ff600823d6ed3d1e246fbf25e117a4986d1a02b895cd9d285e204f2581e4c85e047fdb7731adfe1c3e631b733488bf075fb0bcfa9c3231525007f34d739094b14f492a129d90557284376ac4d3592d4ac00816929217042831fc085468a0703684777c7265d02e18ccbb0349f9a02acc077d0b6d4cd3e1414d18c3fa39c138a0995fb695426e58467536f8523ad301395f29ebe05bd4f17a498bd98af1c20683cb67bfd1460fbbe0d1e6e37f5a2eb6a088f3adcfdbaac0bfcaa568910a5f6b72d8a8280aa8612e0e2a14a4d07c582572b4f116c3dd483fb4e2b90f87c8e7e68b121e745c538ee78b1b26e5beb7da7824eca3461d181ed6c68a1c79408771dbb8c54de306dbe1ed0d76b55757763edff45a4464f8d9d0b5406f70681c705e3e87ea811d4b7622c6d08dbc98aa570b3ee94cd7691fc8630ac4186d11a143a5cb7effcb8235c344438696d138b7be828eb0cc8832d6b462447228392d2e355c9f686d45a181a043374c6cb6d6e6f9b1f80f1ec033531cc99ca5640772098cfb464fbb6d69eac71b97f625d28da942624c2cd675d43e5b5f5087a11ee11acea2155193585312b91b31503d82d6e95aa496730e3ca23103bb7be57b76ecf7a2f879f5d8ffef2b8ab8d9dabfbaa46486635a2c376e68e674e300b51beeb3d9af99a892712e40ea573456460c6de73b14c57cf10f712d2b14a52d42195a51ccfbe29d64e727b10522c7165a4834b794184d2e4e78a01165d82f8782d967a293dd6d5fc80f0cf0f48fbe97222eb3e444a0ca4cdc1a2a70f91a10f88576f99750326fc3178a6c10fd06c1a61986e12ad3fdc12fb5103161c445496183e4cb39802a3ac18b82293621981399a33c7c2b63bf4a0b972cbcf2381442e231c16fb059daeff45c2e8c32ab658e5ce5a80a45e6349e7df3ce7e6da4cd6d8bca9f7467f06d1125de25e8a47b7a3e835fe5b8d46e972e504a4d2c465c94b82668ba9ee191002a752854ea50a8344503d921c8a2ab95062a9c33152c714ed0490b6db3ddff7e680fe745b03043512832a7d240ed0c070214a9435471ab1953eb7f48940101f8d32e2c0d6466cdb1ccacea91993d96dd071a9feccd4417dd2da51fd08fb736f145d9d95d62be6ec6b9f97abc462f5d3f96f546cf8c72663f3996d65be59e9b59ddcb425b1bf851e00db9cba6abe7624b6f77d7acd2bae76653706ee6c4402e4d7b209746deedf0ff0979ecbeed379cc577b834fe8e12d800802ffc299301801f86cddf7e1ff66f55cb6da200bb80010860b9ef0e3b00bb5ceb3f42ff4d779103812bfcbe0af81ac77f6ffeabb997e6a422f57b719d2a6c6c08ec5ffcc2cfa5bc26276f922a22e1ce6711a5445299640a292e33985992f428402fb8813b28432d64833944ac6cdff82837ae38dddc8cdb5775cbc8ad4517175941439570ffd13394c50d6d3d5da084901e8c288963444f4a1bef20ca8349122c02f32f101246bad8c1fef6f80dedf7ba7b92552f901f720a061fe14580e660a2d6cbb8fa6bec534c69193b956258ea9674aa1e533e8a5a3b0bed14052dc7b5b378b426b75ca4d1293c3b40417c521807eb649553617c52bfc0513a4841177391310b78e42800d7d3062c7d2fa4da213ee1ced60fa7eb165ee5e898a374e28b41133b2265ddf351da536077721923f8f9a5882b7eabac5f732e477ed3844b9529ab69fe1d266d9a72ae5e45f94f661bf2ca437a79482b332535496d1dd04f5e65f147741e4aac368935c57f6cb09bcaaca69af0ba05bf24ae9f1bcf69a48b6d86ce9e4199d32c9a26b54b1835894bd7ab88dfea9616010b9e57cde9c90109b52069e8689dda8aa38667d4908b2a27724540aed3c86927720879382b4f7f21e9cfa9da5cc8432a9dcaa34b757718b1a6d0982b4f2e17f444801568810ab8803da88325988055fafd4861a464fb70c34ca5b44f482d85561a4308ac3ba840ab5c0f7cab94ea16904e2980809449cb9042489f1436387eaf0b152a2bd77bbeabd4e9169036298070937841803fa0e2650bd1a0853d1312ec38c8ec4d6df883837a17c0ed81af0d21ecdc3784e1149409df97c9041b224889df104958e3dde5648b415fb8080988025d12201a8e0239f2fb061c23e132d1107e8d4d84f121a3eb1610e2b791e05119dd2f997e8862ade14ba630c279af201a73622f8911c2ab0bf636a228813ccc1cc84c5674de05e34b9f97cd26bd4083b1d122a893dd93385386970817eb23801f19d28916648d403e16c2d0befcb2cc98d067c002217269d31d72e0967bb44ef92a19358dfd2c0d9c0543101a76161ae286a807e3dcea72924268803a0bc621e30b32e23cb7847198a3b1ea296e07b7d15792e4b5637237d7382b608750392721c065878c74335410b2632e8973553f6e874abd64a52668c7626ba022c563c71aeb795145a4932dcbd742c48efde910d947c38e8307cdce82b4e3d4503829cc6dc7a5a3745a846704da4ad7a239c010868df92c0dfca1a0f8216d41286ac8e3594c13646ba2872cc570912a0e8a4782584dcc90b6e821a6195ca2d07b08e2a12056133de42152c57be291a05d133314de2a8b83a2aecd38280a010000ffff5739b993f03b0000") - mewn.AddAsset("./templates","vuebasic/frontend/src/assets/images/logo.png","1f8b08000000000000ff74ba75589b4bf33f9c040952ac147768a148d1e210ac055adc8abb6b7097408552b4c5ddadb8bbb5b85ba168b0e04e90a0ef75ce73cef79ceb799fdf3d57feb867e733b3bbd99dddcf2461aaca7278389438000000ef8dfc2b750000b40b0080be63610200000709bf410000007453979306948f52ef0000006c67795d37000097f88f0f109092460e00a033db6aeab8eb28298a98431d394d2da066969cde8ece803f1e316f116f4767474b77537a6f47072737116f71c63f2d449cdc44fe507331d2ff69e26e2fcea8a3a44a2f0375b5a4e7e77cc9c9cd08c1a1a7a71773b5b012517f25fb17dcd5c24a9cd1c6dddd59848bcbcbcb8bd38b8f13ea6acdc5232c2cccc5cdcbc5cbfbc2d5c2ea859b8f93bba9f70b2737a6ff38f9dbcf2b4b3773575b67775ba813fd1fefa666500f777146c6bf6cfef3fc2790bbadd53f919cdcfe1a9639d491eb8f162e1e4e6eaeff05b3f4b6fd7fc0fe68f97fc22cccff0fe4ece1eaf0e7982cccb92c1d2c1d2d9ddcddb8783879fe27cedbd1f97f47f33675fe4f30c83f28b13f7a2ea26ee90675f0f8630ab49c6cdd21bc625cff4bfddf2819a8a3b3aba59b9b2dd409c2ff17e4dfbaffb6d7f9c71f44f0ef18ff56fe3740c5d5d6d2c9ddf4cf369ebfecffadfb6f7bddff1540f77f07f863ea45546dbd2d1d745ed93a5a3afdd963c19702625cffb3e5bf91325007a8ab86b3a9b9e51f3dfb6fd5ff8ca3fb8f370121de7fc7d1fd9f712ccc45dc3cccec2ccdff3df37faf5a69536bae7f1b73fd4f6b316f47671125a885ad95cf2b53774b082f378fd00b6e81177cdc9a3c8222dc0222027c625cff65f35f6819574b5377a8ab2614ea00f9b3b38e7fbcd2f3710a72f2fc07fb6f8bbf3716d77fedacff6c5baebff62d04478cebff720004a7dac94d0d009004bc7925a5e90ddc6fc36b74d4d685b7402e96b7363e487e8609d626aa3a7c7929f6b6427d818af22364f1bb5853e04a9dc17802f907d58a1c79e0477a0cb5dc78d7621c6dfa71577d92c6cfe7ab7652791fd04ba5786c229e122a56863d71a2ff909616dd36d3dc017f48373ff0b898dbdab8594fe3eb5db61fe5593e098cf48a12de0c8ae2df34c8555311078983fa34d1974fa2bc1ca835f0bdacfdd07dff4f0ed5d1670ebf6992ed126d13358e313aefe42766b81eaf74d59085f7d892b739999baf13ef132c5ae3bbcbfce1e76fb95440efbd0cffe5542b9725b7f299c8a6d39f372568c886e73ef46c623ba5c70639532886604da4512bd24662fd23d45e44db7cbb7d4b0c077822338cc755d1010e63650dcbd704b2b9a858b2dc130dab547087292fed512a173334ad5c29f364c36db4e944c75ee3b2412eebff64e5502e6bac087e47ee89270ef2120125061cbf8fce542a844368473accab92fc7f7a8bc1eb92b6dc5b4d5fa6dc65c6d6450a8aedbc51694628b31f82fe3d8c3e2574115509277c4b683f9a3828d1c075602ecd192e4e113d9a1e4d6abd7b14e2f1627d22a463422b17b5765e33b1eda812781a7be9080d2788093cba0c3c0a3eb839f06f5b641affff89b328d1d50a58a2f1e627ddfd7153a2d1f5aae0196a4f41e276b52754c27f606d520152db889c54f874fb2c080b1674fdfdfbc5bb44f6d08696dad383036d0fa47fb4e4ff4f3adfa203b60a4fb9b7f9b85af67f76f8cce5272efbee58d21919604a828bac099c09e43b903274bef0500832bbb0f473142577a2a75fd6edc5aef6f9c5bf1d76565104b72311bfc59c96f6c0aae8107c217843fb0d45f8c96d9035f69060559c2559b85e026d75b365770989eeda1a63e2440bcbfa804a7ac0c55cd00c7979ebe96064cda8087b857eebe9a0574cc533ceb2f603c36b33a398f629de2459e86d00fcdf53e1209785766e7e73e57174d487e58fcca5837889e9f96d3a401eca076c5aed5f66446fc25c545136629871350446971ca9235cd677a8e874b751e1d05633b51808a521f64f5da6d3d0ba4d8ee7a11ae10736266def2f7e4a373dbd9d81decdb0a6a6a612ac910790d0397d671bd93a5bdaec5d9f93d45f80d51f9654097d437d2df6b9b2ce1d30abcebc35d6f0584fecb845508d8a1eb5fecc6057d556f9673178b180688ae0e9db1e73c49ca04443cfb9f559e6d833efdb338aac06b95116c946c564dc639cc416c3e1ecd82c433ee308e3012a83fb5c046873536f1ef8a922e65db5924953d0d54f3ac849d714e660c35162449fed6643d04d031d66cc3d5bca2537a450f14aea79e7d94fb9b311f0981cb1d4956d2eeaf6e5058161841c01a27de7a553a6b26587fd0fe2b665b9a66ca797ca5d8c4897809b2ea9d68c4596af0f1a48038285ba70d4eda7ba58810ad1cb45ebc055213c7be8bbbfbf57b4e7209a220918beac17229822383ae876bdf010def190c927735f3a6083ec9674ee342034521559e10c623c1f0c864341f786b5edf6b35ce9ee936f5245ebf63b1e2e1fa29a4533200fa8313f05671d91f9e16bd3e13eac2eb0cb65006ac8ba72660baac49c2bc765152e17cd922bf778b302282691c2c549bbe372cc2d39d4dbbfe534f03c3f671bdfd594a2fd40c7f9f2e5a687e8a26413b9ed83196f93676dd2c94cfa49e64f3f12848a32736c49c7ba687b6e07ed14e1c5f4dfaba5e37516daf9cd0be4d2319a3068d4ff8ce9d5834555d203a0a6182e90a2e869c93a04c0b4ef6db5324aff4883f6680bb8937b67016f7459e954c9447e96f3f7a98b2ce9bc8dd3509e03e6ddb4c8169ee92e699f2480c59fd307b05406e6d5ed95d4d3d9f8583f8475c153c6088e93c5281fee319daf2306fc6ff9cf76d626d0cfbe064f2cdf6c8b5539be8fefeb123172611d2f1855c52c00176ad0dc3faee4964a7bfd06e01e35c11f7bb2162d5b33d067bd171ead443635042588813e14acc95b3e3a9ffe67f147cb6619de1caae51bea4677565102039fc5219ea48ef53de026f104e5994051e56e6752c1688aee23e896d636035833f09aa626ef1d19baebf1b1da0b41e7763e6ea8c1c225e85c662a3286d63d22e4fcb2fdb2cdfaf8bdb271dfcb4b62c9c1a044c900f1b3b968068b965e98cd2c3e492373d4cf273e598d3e89398d11fdaa54e652824109d50a1828f5582e61315931be4e74d4b6ef93079ed0addcdcb935e2d8f3f3e825cf05c7d045311a4ac44f05b9666c91c73a50e44f7e8fb7ff648cf46818566e216d30310494681c885ac7fe110da2ec20a8ed7b9f3d1cf31edd0712b7d15a161d564da6f685d8705d6c8c12f74bf8d0d6598f828af8f9b4028234ed087df8f26041b0ee3eaa60a9c5ebecd636d833f87a47b11eee3af47e532e450b4a5f75705494eef7650cc1e68feb42ca376b2c57da9eb172272e7deecd8db45dde6fbc10ad9aa2df6e32c309a338198ed630842b3e9dc491e4fe9d8c6dad40a9f9cab7c4378b0b123a0c563bcdde713f0bf0bb1cbd7ffbd58f6da23e8c20b2fe6a79f93ea37ae1fcefa328ad91685bf2ae5bfca20df16a25fa7e9b600c03daf19978350d14cc0fb5771613e6c026497f076551c820cee7ce78519ebff9032cbec170348f9d06d735294f3eb0bd24ebc0d211fcf58d3cb1d301bc7bd947e9014fabf6bbbc2606cec1b12603c532a1e8e1129f2af7d982c2f40b06309616ef1fd12f0a9facb2e35c4c136943bd224e5e1d3b31acb47a0e7f61327d6142f30c8412a15727f36335e8e5b839721cdda2133a57306e3e6527f04b72dbf3120a58c5351e5f33ddd9885559feda3ee85ddb2848d831083ffb5706130019c9f8ed1209653376123c06f27ab55cdc8676065963e2600240e8c068b7537342b30488a83da74e1ca734ba7ff53482997ef3359880d65f66c417fa11cae28c0fa7c978636c6c4c07f3d93148d7f272bd9397343cc4c45f081c2a69c93c0b4dc35ff45c90a054e985838ceee54653f8988cb20c5ee2899126e1162f91977d2809d1fe2d4aae1f0573b03761c84cca913621d0c9dc2ae87fc619ea2afd7bcc8f5382c75811a2f8ebc6677d88aeb4ac0cb1741a7734514bd34987dde8551918fda98d7d8f68fb4ff94d1a8c3fdd809147160dc7ba3da17b38a1809dd101a5330868fde59d1946cea5648039dc7c80a5734efe7876f54731af949f36652e51cdce08704aef6f92161facdc5ac050897a88f4453afce974353aab29dd8b9ce65f14e75a59364be2189341975e4197c81d77a4c5daf1532783f68fdb624e528a3806b845286f3e19c2bef61c5e41de192e78016f77f9da5624beb06184831f9e59c154a4e8891c85878df3a6c14a668083013ebca5776aa7c5aa666e4b6140bc0181c715ba48ed38bb28e907de5b609a1afabf4b93277aabefc53493b381dcca9721aea07d07119f6791b79cb067359d181c68546842237892e922268c88def58c316fe4fd49d71c6e5f8ba7a102623b2ae44c4399a22cb2be6d03fafcf06ab09726f83a12e0710f6d4224d024b66d47ef1cc290f09b36b8dfa32ad4668fd98505e50d814e485001fda2ce53cacd81a74fb0c92e1fed9f7d91317b0ea5450e2b45afe68874064252d4b88727488cd8f3795808e8c9bafb1214ab58ced628eb3b78685582d09c0369480057289a7b2be9fbf4c983e053a65f709acaf658baf4997f329c1ce3ca796d216df03a2f48db002899a964d42b13011f52f449b0e5487a51017ed51592dc93bf43ff434cc8ee49c8733e2e8ef7b033665aca80090bc703c152238c6d964c0305bdf65ade805199bcfb98676f8402075d531fa702f334626f4d4d02488a2a384b6ce9978cd9f85132f225353b78790d014951d55f8f4a684f63585e144c1506178d597eba519c0d027f262664aee12b31640ffad2ae29971e1c9cf2fb99e0f1ac3d7b1815c29f35581a187823958db97f67d7d9b331f1f5083b67d76d3be0d2a2dd7bf67e6e3f3af1e2b067f82b0ca58ccc0df688fee8547f0585c3c395b6bfff7d678e5ac772cf0aa4a65b24aeb36ded42f0ec4dedb0636c779b249a701f89d0c1f2014ba1e4af632427b3395f0bacca72c823670bededb9965abcb8248c34c31004a1c3bbbc0ba280b35e1ecaae8ef650461511cea98cb9fdac8ffa981f209ad4a0692fcc1d414e38d295f4f9dd5bc26693bb5220f9f7c2e01f525b445ee57cc61c021d34dc019c653702816d670ccb39496c90cdfa41d533963a33445a237dc615f6ae61caa4932747e13bc38dc7723936e460c86588e739257c92bf9922e3a11baf4b9421d03db6bbf9eca7d9f7e9a537bbe2df6e95b3fc59e60f80e79ed4f547d1160d53fff003272e5093c332ad46c511911029e30a2572c02670d54cec0e12b3560e74c3740bc7c340bcf959ae06a208f5d8ed6968a4cd263bb5cf3bb23f30c2d816859d7998a296492c385505cb6264f14ab54756f976f18aa0996f82baeb8d9f92e441c1efcb16dd1beec0125799741d3a8a1de99ee29184fec48ecbbf52288f4766713152233f1589e87bbd0e15aed07a6945bfebdb6376c24a5630af79477f142d41f2167a8dd3dfa0ebf7aabca77faf706fef68c57461b1bbc8e9ae1368264cb934b8b5113b176bdaa03809e5727470e0382f3b5da6a80cd48635d7cfbbfc6e373ecfddfa67a88736e822aa1db95f77fd097e63618d17c60e7c049cb4a22d291fa4eaa73df3f4799d5524e9f6a39acc86738f7e3d88be80afd3ea14b06e86247525abee46b097ac50874d08c0689d21948e99a2a144eeed5ef415338c4195653ec17b3114cb4c214839ff7627f6a4375e6c77a535766118f94c05f0873bb844eae0d1a6dca7d1fc13eb02d35c5dab1f68ddd8aa3fa452159a0ebc0217da6a9631baf10679533d386c481068db01c56b1f604d5eac6a16ea2fb8c78854a94af38792ec8347b4a81367550a6f028776435683115a4f72045f724ad39d963ca5d0f7219c776920369178ac03c71f6e48e45f89b8562688bbb0e412fdd441fcf7f80f83b116d78d79e068100f127190d77350d07d55747aa073802f83ea31d83b9f4ec180f967b335b089cd0edff077e3f1c853d8c88a1256dd718e3768b32fdcfef95d7f7b009893384b3026504848db2a34aed8cd04456b35536c9518d08fd65262649037fa4083be130d0005d166d7e8ecbace30d867555844a2764fc93a51e6ba4ea701eb3b1b99d5dde8ce514991cf10b2e89dd40025737b59f14a3107a9e9e5df9d995ec968ed789927c3d5240a33689b77b89a72999782c0b5de58ff84d6a20ea5b9f051dd29de8ce3f7b2e1a84cc0aab144ba629076267e3a0b1d61d3f64269d89d43bd9fb9c8bc9749bd5fadcb693bd1cd1255832beccbea450338291d8e5d0a4d5cc41f6c484440878cea87ea4f2f2890ef9cceffc5649cb519c783af0dc3f4129b8c998e93c7fcad2e5f08c26fb974e1c7ea842bb4d6ef3122d653ea23a166f23bcadb5f9e9f59118d5b90142bf55e9f049aecec25393dccc60eb89f6dc4ced66006606459b3cac6d1d617a34e07ee777afdc7cad138e2e756c27ea1f9d183b32ab2909191274c97b449cbb0e58594148170a7a6175e27d96a43d1de1fe8efdebac29035a322e3309fd436923c63d781678567090947182612e198109e3226cc901dd084d45898d4209bb2c93021cc8c52b53e0e74544d4e9b1c90e8bf136f13502b43bd915bf174970abbab7def77f6b3abdc21ebb39ebd82a93a025984ab9b3decaa5a685e89f0392aa7bbbe014b688b68196174bf940bec8c96a5baf9dc509b3ee919d33ee570eda9dade56ba8c39192d90da1e7ba7fcf7c9bd4dd4d84254b477b2f46b12e1877e988b657062ad2d162046d5469a775514121bf24ac201ed773a0aaf60f834f158b23217fc495d7b517d7316e0ce95a42870fae92b89737933130809383544e3b6e63a7bf7bd6e57da7b6ab317199471d64b21b9063ec292e78219553b61ece8e74ff4d3d9a78ce0a70d45f6d4d80e08d73a6803ea1d0b2c974bc80174a72ff1b1c3dc568613f4491da860a5fcda2e5fba431e865b0924e1f932f71b31b59fbb0617bd1d5e596595488a8d72c5e5a4e8d39ecd538e70ad2a44ccbe6f9128483ec06e0b158b6b696b8fb7ed333d12b2d69d64e0b16d5db82c91bc5b06ed94f06dbd805f2cdced206883ff1651aa60486ea035d30cea2811cbc0a58724da1aa3878bcb6add2d0d71aecd6168ba3a6fd3c5303406ae644db240bce40211309358fea05f7cd26aa52617b3b576503ae83de0deecb2802329c128048dcc50bc80a6b94fdab82f380e6740c13b07b6eaaad7fb9df3b15d252a5d17fa820233123e93f777a9072fa21c99db46724a1692915a31ed34597c7e810287125008dcffdae6e5125365f965f482f359ad6a4a91b0f3bc2cbefb200a4d98455146719aa1677b7a5ad86fddb634a6aee0db4d0e4219567c9614467d1be2872f09444dc8c53c082fa2baad824f7e02501392d79e26cbdea83573ecdfa11f79fd71bb224414cc305908e6960c63026b75ddf1eaa002291398a30bed1337edff2adabcca423b6f216ee3f4ab905b59a77bb82ab6a7b29740c3b9050256388166b07c7d400b8077686c005b1dad472a374447184d546adf6caa3fb24f10ed3e3002cc10442cd42600433281dc13a42b9139f988eff1443ae92defefa8ce7c52d5acd371ed8d929aee66cedf876f99df4ba0f51bb32db8fb0e11495d672b186e2ca7bcf6baa06ebc2ded713811eb57e93c9a9e2d211a2105d07dae9d5b209412e4d7ad06c1369d8271fc5f13bbf0bcfb297d3f5cb925f7451376f8c508a145b8dc8d9fbd489025cef32eec849f614b4a97c329e7c2da2ad56d6d3025ae2767ad572febeec6478eb9f66eee914fb2a7bc7ea7ff8d62f39e29dbd2a07a12ecfc063b4f5082f2b9c464fbc979c03fb4ca305212bcebc5ef6a786814d919af6dd4c49301850441683b408f7cf4acfaaa7462f4a7ce65c9baec6d40fd8039a0a46c167ddb54478872e71902b6bcc5e4b63401b8ce5eb9c51ab1d04b371e3418dd1cb56a6937e711b962baaa13481ee64a52f60a901ed007ec1c2e7034a30462db03489744ac2781dbd134635b639651d77331cae6135e959e59eef318bb9ec615fa0382b86b9b9d9e5267841cebdf8cef3579e8db699fc42b18bc38bd5066f1e2c7567e0f37df179e2a6f03b8cf8ddb7a63207a0d6848a0afe4d62f8f517375035d5c5102b2fa7cdb0077a9e6e7779dc127f3bf6d1884f6a7ddfa65eab68dde7b4eba571d372d80cdcb2fc27b4414ec950d15ad0a5d9faae8196742b63bbddd07afcd388c24480868bbbbcaaa1c5a7f1b0c5b5dbd9caf18239effd100001f001a03a8f2f52b5d70055d1b4e01a723b13db70a67031b43e3edc3c03b104aff0fb2992a5c0f51397ec87ef86c7b537aedd98bb9d230e5d7f4cf328c92eddc7354110711146101263b48c6917ab0f375f74784cfa45fa20198e800bac27406c30bf2f42de57a90c0374ff03767586f60aca8bb9a9e350f584590f84b642505f260853aa61e43089cbde33e781a9ea2e5c51cf0e3672e7876a63db26e512224be0fe804e74da76d8d293e7f3f9a4360d68aeb2f3e24402999d84e2cff70aa2478470c3b274013c9648b515670ac4f9af79cfc86ede89659ff76d3dc9b4dcbb02dbda7919efc19fd0183e727790fa705b4fae52e1dcf801b6e51da26bd3e2341d1de0989d845deac53ccdad6b4df866cafb35e4004b087d3d58d9707623247b7f652d74f7616e9c430631e24184ff8bd2f249c1d68951ea9e41bfa3928b4e315adbf2b7f338d1cffdd882bf570b6ae706de6ff7c36e0ed8b9f6ab247918903fee761cf198ff0d25e0eae2a65293032f741f06f676daed817062347f1dc8bf67c3657cec62bd6cfb491b154d198bf457558de8a9fff45ab171158d324fb9ce15e5db4c17e9c204e3753c7933d29253897f3631bab7b2fec021c57feac1940b1d070430496344a6ed4f159f52c9fc27510073eededf3b85c1032bdd3676abd1d26826b053859efd937d82c0ac2a21beb161b82f44a28dedd7ca908466a00771a6849f2824ec28d2d84c7ae505927743934d1d56d288cb1daeefaac258a0306d26110386bde2ad556df1087f8953d75f7a60c3d4ede2e51adf4166c25984cba3370dbf93bd9874533a32cf1e3b6f6c8cfdf260f1445562e1ebfba7fe6b8e9180be64d0d0068720ac1d314ef0520629f2cf760273d14921b726efeca7d83dae17927b08d13466b0a3abb8f51ed5a8e4245b1726fe76d1dfa5bf83984efb17259af1705f129728befb2883ff6c61bec9e03b1d7484a9d200d3c6aedd36f0a6e5581d4102b7de02f09eb43b9d929608515eb4c26d183cada1ecb1526620a7e6e08ff45ab20b41f1a333dadf14fb638784d9f98b160be7bfe27972cbfae91f0132180ddf3c2b64a126138dc5a6783665ada99b1a0c345ec72c03ba0dd79ef57905a998286606687356ffabdd0d1d4b29e01e465de485645c89929f6799412491e386b6c4b054df39e5bf560187863c49b3ac20548121e624ba2b39aec709f4e76075ebbe34866461b0bd18d804c5e27dce2deeff632c1fc6c540025f45fdfaa3deb05f7177d90b0ee26bd0638e47ea6f4d15cef1d1f604c6a60be1da84fb6c29d390e361efa5ad835a9b87f99bb12b9be837c911036f066f8edc3528fc5eda5cbf51e912f48d12f2e5a381207666e30effcd4f6675a4bb05e72080bc00e8a8fe4a31e2d5aa06eab5512d17834e42fcf4d92140a46f5ab56f912678d0abdff86e0cadd6d1d326d95a974fdfdac828ed04c63418b43548dff3712f33bca960a2c7aa210bff894b3f74c6552e5c1fda5be01da9dcae59855a6ff265f260a432b653ec26f81ac0efa0f0f60031969f8ee1235c2198fb782eeb70ad1a8836c4574fd1b6045c706ae003ac0d78cae18b18de13470241e61462bdf38b30fd8b92e11cc6edc1ce4765fae59ea402969ac6f6e3b5419e82ba02fea908c4bf0d18ad9404d4a64c86ab4375a1bd43c6a51fce908073e26fe831d500a0dfd98cf8997755373c91f5ebe7f81e0045c7bf85f0117efb10957b4c1986b810b85b6d2329db5e3a29d136992e312f21fc124d59d51fb33c37c286ec93cf584eb09c6733c930ccaf522a7ef5f2b84ac9d44ecb265701bf1bf29e335930a028582689fb60d30a07c6782829878bc1f99b86ba726cf6411d4cd283df2b1aee58f67758bc4335ac97d5736a494e8a8e169a3e2f4e4f99e476bad52befbdc26bab41ef1db969211dbd971e82e73f6ed60ba544d585af9ebbafb6eb567910442b47a1e65a74cbbe04c16dbc4d3fbaa0fb24a95b6ecf0904e6a0f8679cc258e40b126d29e20f9cc7bb4e6d1fc8665df95c1b351d168ed1f74f88ba23041b6bf72465a235151c964d8c2fdf2e90d0f48dbd007c127c5f5403aee9d42c3c3f72d79153d00db1739520a004f138bbd8b093d05045a7c281cb102959583cdd2bd8e57ba4a64cffd5ed9ae924a92c51d59335f393bd3f69ad680a6e97a1f1db11f236c4fe401027a06182bb75d27de77b1d570a298d1968e427b7b7b09189c4b768cc9d1c484ea0b863bcc705e4dd229a9a8ac6efedddbbe7280ffe154f64b211ee0676655916cedca77bc1a3cb10ddbddd83e4f5e38d679735ad77f3de54f7c51bdfb234734378276c6814fdfc877dc721bad33121b5b73a1b3485606ef37e7949b55e5945d144d8266912a32c91e5ac4ea6cc05bac48f556609351ab81a7becfa2ec2280587566aaa55bee578cbec19ab90f21cfcfec75dd86f2c7f189b33cf61ec50ca18f030814b631d610036aec13c7dd952ef5e917863e315e2ffc1d04a8049efc30139b1b1a99ceecbc00fb8b9ec881b7ce8632ce4bad0de57c92294de0edc6bc48c5bfd3870b05908b4bfc1c6bb190c89994e6d519f3f8d854080d2b3a3d8913b67795628149ce5c569964ceb0117e129b455752cd614dcf0693d8302a6de7be27d11f67dc186ba9a1c2a8024b7eb414b5ab4ea6e3a918e26ebd4856b3832159860bd0f7f3c6c585b1adec1dac9f209f619dfe2c6a2098c4d8ceb33ec1bfca0434887f777a0e08a865957c974ece9319ab26926ca27b3159aa1060c60d8c66948c7c69ed9ade45b22d670d2b5949a3b42106917c1bc5215d85f2f5e99bfe0a5e9bd503b04ac70a4355069a3d84ceb17d1c9366d1ce77e8185945514635b35ab3ed44b670efb8b4e1fc8a5703e6f65ad01b4f7a813ba3f783402ffd15f8bbb834113b1ea53af63eacadb413fc3841abdb96014a7f9aa8806b5217643d0bf6f537ba10b3aaa1d11b869e69d3f085d3e44d19cd02660ef6b484c8a0018a800e1ff996e32e261d82ea5daad26c5baa403a039a17f73181976acbb96e73dead2e3e7e361ea23a5004f961eadf24609d0de471275c9e324d752619d5697cdb134f06c009c2273c255c0d77fe18b9dd1d8955022b1177cb7a669f0bb2007cbf6565f603e0fb652d2c5883adc90e97c525e8e533173160e561acf4e9a12a1d14fe57ae623e11463d1d13fe12ce27fc71cbc35b9e52cd80f4592c08ae4a1088fbeab975998708bd4afc1316a08bcee3a70b20298308928ab2c7dbbf5ce92c3f8c4fb30a76d32705c4b0cd31bcacc4b656026e7ed1028a772f81baec05e9475c4f44dc0caa69b31f5b1c2da911ba1591dc5077ac0a4dca5ad762a4e9f2bc3711ab3ff86cc7e7394e731e970b91f45c8967ae2039e0d6e03c566e096df548ce4e04d83b691e73052e946f5b99cbd226a1f7b4239eb960f6b3c4ffa896830832adbeb3aa0c37587b4193f1bdcb98373077e594d691639de68dfc3754f807476a07eb39c525a71c806f93f6c9f45157c8e4840931696eeab6e8c3c7d9f0f64ccf5b7cf6227c9f0545b4bd85cbf7ec01b37f53f208d9ac72e1bea2b3a5037316d0684e0dc71a1c289cc1e66cdbf956f51b855cd7e72c59074610089184f7a54abea92b5f8fe0c56775ab5a3a0012c76ad8907df4d175fbed7814df7683a0df68b3dbb20f44b75d96e8448e83b426f347472126758944671cdd1bcac87b8cf39ffa86f533b9f66e1e4acd9692d335002857f7255f8f01681ab04b12439503881904089b66f8fb13eaf676b7f2700bbce1463763db528a74c9fe3e323c6825c69fffcabd9a00309b38a97eaffae545d2e4a866828aabd585b69f4bcead5da77ac79e906deefc0a8f12e78a862565691bffa8d15619ff3e937280bd76716e9ede8e1a54f843dc4f058e0441d12a7a153d22610a8dbc38313172d6ad644a2fdb51c0441903abf12f19ec6f02cfe7f0eb1eab5c628ee83ce5e495ab1a646fd072c0dcdbec4c653874aecc65f0a812c4e56f4faa8a7a20f754199c4c8ea9a2cd08ce146174279986335cde2f38b51c546b280b5b0665baa8fcbe2ef8cfb5b9f10bec67908a38e8d20efd7e8deb7e4d1fc80a457398a632a44c02e5376a3214110188391e3f7d99f5187545dc88518fa6f783b3ad33a226622c5d71468251728b0581b21c67e272d1d54fad4099418de81a714693493e4228c4099cfd80d75ec24ca55ac12cfdfb9492373e80d69f33b954b55a31ed55f527e2f416b48f0dd38a31bb74c5c057bad55a183ac578d2ef25233d3ef20fe03e764c1b9eac01d24b8f391c1352a231585c963dcb375c5a1ed63985f63f538dbfdc7cff65dac2b03e905fceac8823c511d03413994ba51b509a7cd0d5563435138379e7a188c8c64cdeae7e25f66bd6695868e303998a0c13fdae355ee01577c285d0eb685a47cda99105916236ae050f26ed6670ee072c9aab38efc1244bb11e3aa9ee85ea04293c8bab5ccf21338e55b935ed4fe6e2298c9fe58469bfd5590f2910186273b156255a4d72c1cfa16267865f94b42bd2aab136c3e34114bb1ad40e82e26f378e23b7fc8a9b236efd7cc65168629dfe751e348d541f33fe2a391f606d9c3a3b0866fb3d4846c23a384574536f3914c48cc81531ae9f8f0b782b1ee1eaa07db440012a9f65bdca91ff780dd23bc6ad46cfc9a172d534fe384ea0cc0cbe7feb58020838e70fc74bdbc280c3dcef5175832fb2a2241ec8c5258028358c2280ce2d5ed6fe379403a7fc43b5c03b3acaba45c9ebc3a5bdad53f3248ef855700aaf6304a675e26b6da900e9922b87d8c97acc77ef67e82913a7f1fcbfc627c625e5ab1360acf7ca49ef70180dca2cb682b1b2a20270181e31825ebdb44061009abef94e97b65c955fff26b763e8960016f5daf1f8b2bd20cddb2c40cfd3197aab096b6fecff64f64d10b2da30ff36bbd1e4d77a29ed7ebf32a441687b2a134b2a1c828ef9f623fe66330faceb49e88abce804efefe90c1e41ce5ada67bf8ce22d7afd5905b7bbca796c04992d9f50a04a0a9cd21a5bd82b4bd7ef135b2105166a86f34ab092f66bed0172f0826a798e9491595a2a96f9189d28c9408a6737791c475b981279a9004c4c55c90f1e9df180653316ade631ee74ebbbfb673d4be88271a5685923ba539c1698d09cc693aeb88c3dee9bb352ccfd965e0ef0baf3f33b0c7a184db01e4988004e1a56184893900886858ef132f7c4af6f1d4d027355126dfbbcf2f9913e922eafbd773dcbaed41d0033ea88159ee388191d9b96f88660e49239d0408be976a2cfd5936604a474d0cd058d6e559e6fef3ec93b9aafdbfbda78165e7b645b3ff1299f62e37b40ed109f834d2a07a9be7ea2d6f8463185b5a97d700d4c7a81cb1957a93334ea8374b75d6f50ae7d4e84abfc2ad924c1a3ec266ad9a443dec4e1eb2fdc181955d9623ddb0ae5384558d0ec61f5c2602b8c60ab2ab662b6ee30ce8f60637797ca122429965687cb64d3a9f6f9006b49f8fcb7152b1b63877cae98a91d86e45809b53cd5f68f8d9a76adfbafbfc757add93b64afd7384d7f9da67d9081abbe027aa438c25b377eaf7428c9dffe10d4d9047f6dd4bd82a2df5514dfe40890adeb2658fbfcac241ff17ce1adf77295e664242a81ec2cf487076640bab1dd6b5db3dcb40b0f1b1b9cfcab6cae773d42bf08dd4a19a2fa6ac64e4c4e5a06da4bde7bbeff8b14755691043fbaeb4b185a5a00fba297b75fef5a87500662203bd1727f72b06358cba5bf79348d6d927d036cc57178d4297b6c3bf8321fd0dda49584360e0a331ae210e9306a4ed5452a19fb2435343daa30300871eb9ce595a0a533e0f4d0e5989975de019fec351def4f7a2fd73398a001b003813b47f98760d603ac490f687b13ca0036a5c20048d29336ab7cdc8e439f5dccb96d07d0b3b55933c5c036d2373ec84d32e879638d6e6b136e5e0e7009ebcfe2af8a782164562dfd64a5d8e1c7f3020da06369b229df76a439032705db35f627522a2b27da6246470d6377fb9775d2410bf67302957241a82123a921bc7e3d27883d7ed3abacb4b7a94349fecc0b85ef9ae4bfaa999e997f26bad65a550af510663ca6ac5ab42962d99d7e428ac4c75128fbe8ccefebfe06377350df28474f4cf64205514417898a9f44b1f4fb47241f0e1ddf50b67f5ced8bf47f2db2572d7bb3bb745b26343f422606b976a05c7ef6b64aed6a732bafaeee63079f3366dffd2af5bd9ce9c56af7f52374e5be50b955b4867c5f931fb86965f25ac30666c41d4405e2b3b141b3def5de97bd19e289ec1356f3239760b548e9bffe6743508a554b741f8cd0badc4c8ae8c4a36b311d7b51b878ea9b209b4607c0b7e9af4607bc4253b000f9a31f488701f80b01183aac2d189aaa4f16d5b09843ccb97603a99be0acb05102d2c879c644c37be94dcae684157f7e583f609ac0c4fe5690b4a6ea5e5cb90dc89f01b464a6581e3da5455ead6c8fd207b89d7158e93ce00d669077e3b88e7088f9011d60a53828748e2f70de368a2cf4baf9471eb68282dc83fad6cf63d486e3104f19cbba0b9ddcd1cd303ab3c8be2896d66f601a0ee2b1baccb03cc2321a38ad107bd8562e8a9db577ca47bfd1e484929a7d47ea119b38f1fdfc593eee57dae3b59b301464268778ccf438d58ebd65400be3bc9d81c5d160f5d2931e3a1086814755b22b333954e0fa34ee1353d7877a9261bba029aa755b9d8dbbe0dc5b755bd6303c129fa71e6201c22ec5053aa3ef73eac5fd73ed6d900a9fcf3377649fb5afe95e68d41ee33c006b5b52c356df2ff8e8ab755e2fb70177dbf5e2f619b0c73321ac2e0ff7774da8c5dfbfaf28330cbd39e0f2fe344610d2a0d11fecc03bcea3fa56d3ea0aeffb9d3c171ef9c534a3f7c32dd91d6e6319e2416c4dc11707b0b678c44ef1df35b3f64849ce379765034e07de119d8590f3e23e3ee23b7cc2ae7574d964350c428ced307cf412501e8c657086849add99ea18bd3246b9c2415295133031128b5abb29a43a63bf0cb6abb79a3823be5014e19c49c0b2d277c6cd0b3df2f34e4429a8d5cb79cd7334ee27d8b7788f6d6e8ed2a75305c7768a71e5d84aa0475836853c66ccc8a67194252ff8d185d6187e9a9dbf81b8e8501653181dc44cc0c44f60e9813eda1df37e428ef792ffdd846addae7a68f10c844060115b16c321447523a4d023324fde2adbdc326b272d73dbbc42efea6cdcbac6d663be04441157ecb8a443cffcb9bc2541d3c236dcd3ce4af056cdc8de642df5bee97e29dd899ddc99637c5a339921f6f92b836fade780ef8f7b1f07d589e7390b0a3211e2dbd53d93e803a83b313b26f6395756217beea9021bb0dfcc7d0cee637977ed6c011f9d15afa08246389d657f46e370d74d1d4b80efa2371e599fa63be9121ff4274107b4234093e9e6828e3ff720ec2ef25ed0acb646b7c0b4571c7472410fa2eb7357f65bb1283813375c3e75fb66f9f43768de5c358cb8b6c4e6a6e2c4ade668e5802793a852d0b93d00ee80fcbbb4dc19400c94dbcff0453f7c870e0938ffa5027e7be75740a68bb9a60ee88cea549504d012aea13fad26784a42fda86618c421f59586a7e2105755c615f0763a00ffd0ba62cc4a1bca99d8e400444e8868c03b493df93146db9f49893c1402f183be246f1492ce1393878387aaae0a52db79cc05165c9a70db1fcc2112d1cb7dbfcb175fa9eaac8dd3f783a27f458548537e09d4777c3d38ff8bde72bebc281393992f57219fe30bf96037af6276b733ac8647bdb0f550c176e79d67d6873431ea797e4b63cbe00284c942b7e5781c5404dd73b492c0f1c73b7d65229a41a9215766714ccd215d41aced48c80281f9ad259931aa8a61ee80abbb4bf73acf35a4148f650077ad549b5a3d2ece449d8ed483aa2f1f6ccacbec01c36cd965dc09e539d6e87b6516cf94fbd1e2f7c263afebb0b237b4c71ccd8e0e70f298f7356609ba410156c9d8d70f180414c6d19f9ed23a2c8b019cb76489c7dbe17dacb22d8b31bf47aa85ac302a8f3266a1a0277da30b5ae3ed3495ec5d2367a79880e58a75d54c3c14dd75575feb4756d3a7871e6efccd69a995e1d977a16763120fa41e0ee0bd320743953e2f74ced6e31f50678bf4b142e395d90ce8352af301850c5b6ff9d5e6f167aedc6fd45c0946985d6e065307ab18069c97e8e29cba7669c78ced2a9698b036f61ac3f25686cd0152950496c131cfab2ad524bfdae1f060539783b3c4cf3a7b6f7b4fbc51f33d485deb09a6edc535fce78631ca23825c9bb1da6bf3dab8836918f8313f621424154182f71596ccd7a1cfad0a1e4bc6cea918e19ffdaa99435ac7dc3a517d33aeff30c06c99fac83707a7911466c587315e191736ff064b4dc46717270e1780113d2833a5768fc264f00471e4dabd2ee4380e95792893538e75e2b0d6556158ade5e13be8471f1d24b46528684eb3d60253f1da714076b377ea2d35ba9ca3e766161d6255d07e6054ecef52ddd6eaa3971606cc9719eb8c3808b49ec8e88729f12262ed0805386db35b85f254ca467ccec202deb855c9c8829f4e33d7ef25c9f47946f9cd6741168a5b49d9b8b8a303dd5f263a88e6b0b25c402c073b7c8cd92952798a0b4e726f3fd98ad076848013e16088f5b7742ffa2ecc00bc959a26fb5e0ed19286240341a5cbaa275697900b341db860b2c37e134de5e808c2176d4f6c31cdcd8ca4b26634c0fb1393604326a0aee5d33d1ee546c08375a5d5fe1ba19bc80f97d4f32eccd9de8c2896c7a3b306b733e1f051fde8e5477bbf4ea12a3495d0e8fe132dcc3ce3ce24944860984fa26031b26262c8f8befa2ef664c39736a4fd1b0f2ab6f62a36e24e7b0a2c811ad46effb6c5f3e7c5b3b1902946614ebcac764ff49ea9b36542c5d4f9fe322be1e8494458dabe223025a70868958cfb3d67c6672aac9ff9a9dd1300a1e5dabb46266c4a722d0ada6e7d7fb883c07dce2df36c76ea7777677bcc8085886b33c72a487264e0134b7826828cdc8e9ec08d67819066c55e22eb6421f9c5e34e0ee32e2c727f4c55247fed6e6d2d6ea68395a4d96d2f5c739b6ffeb56795fa01568a8629003e2ef045b6d8aa10ec4c4d61595c42e38b0780c83c77b6fdf8791c901d6ee6222f6d5ec2e8dc3bf9846c5d239eed7adec9489a854e7c7798671a5f0c6b0fa489f724d74e5e78705c1a5f8e83fff53e986670f7358f7481038f9e03958f800ed57ce4d1a9af9b5ae94c7ff70cffcc77a60a5aef94805fc20b87a57e27148f064fed55693cd92e0377330dcfc3caf2b914b6564207c8f732cd4638e76a195efe2a565cfab5e92fa39e4e716729f5b5c7ff8294f92ec852207943563505bb33834afafadcca6f1ee49a0a84faa1ba510da99426d0881258dbc4622eb4788177cc3fef366b07b90a2e98aa39b93a5abf179cc2cc403827cf5e3d9ee97a304ea33e821cb59e19a8c3ade7f07eb8a419dd9f66b6c6f06d68eddcec1cfdda11b777751bc95a4fd6dd2a2cc4d8eab270214674ac68b6da09e819b409d01a7b38f376a4083a076012664286037ffa2fd0f531ec2bed1135f631ae681514d2068b9204db57b45d44cc995dd04a0e1377b3e1813174842d3488b29e742b91e58aadd482b871249db7099e3f79c2f39105cbed2a33922c0874219cb9eb3fd28c29499693186b4653381efa8c6b4fff77df336ed8a69c0b1085d1d9b33e58415f2e590c32f99d9d63e55a2590d9b1d1e9e0e9f264c62ac0d1d19548e7e806a373a86e54c8ae45fd1d64b66cb88af199333247ab8b9b4bb1c6a07359611eb0f963b69464e4c5b07f9cda3015e5c5a7b04104c6c433dadb9d80134bed13b2d2c1e240f6131e7a88dd5736755890bb64255be9550255e9aaf79b68f2855a55b9386b266f927019ece8d45f08055bd1f893af6fef12f9fa42eede9d91e40d4ae2afbc1bad650c907966696fa63f185b1d2e31dd1f15f59ce2ad258ba3523a73d16c905962a93bb23768276f52b5357b28850a134a58fb3a87f124e2a9d127a1f1fc67ccde14ebbc4c25706c00e2649b697d00d9033be52a297fd6753d4fc9b737fc5db40a02b8c0ccab814aa6cf9d4426404f172ebd484a4b0a811724e97e02055341fd4ae3f02d32bfb1d5a48cd16c14de2efc366d4b6cabb977d67a46ff6954b438fd9df6d5c531ea628743cd5db3c0d84fd99bf297baf0a0bbfde7ba88981a8baf38369e979e933ad46350d97d02d405b2c355d900738700b06b6c2661070bb8596ecef8cfcf0c1976e2e7588bab58c7e3db1a008e238b998d5acc477c9f45b9c339df040bc0b8de003d27d54d98cc883f37d1111a9bc492858084e9a4622a0cc8929bd0ba3f4f8163ca2fde86f7047b60debd2f85e9c217a9e90cbc6810eeb72a4d8dad58a92dad5fb2783cf92379203fce06c15863644788d7fb63f86178f45b55c76dd87474da6494fccf8b437f655c95c2c878922839891d367f85d52accc9eb852daaaa0fee67a9e8bacf3bf5ebdae544665317e3505e47c4fb0f3ea5aa03864c803334c2ae4d3c3ee9eeb34f25e954d40b77ea67e68cb5d5cd5b8afed648f44d1bb9fcadfe56ff0c3767d5b32013ab201b479ada3fdc4498097b41b839a9eb9343ffd38479c14c21fcd343f9c59a9efaf8c138fd8d49ebc186ea4265ed066dbf38253a36dc55a842602f9b92bec35e56f8ef57b212c33912f7cc5c2d781a1d0b0cacdf8f580b560710b23f9aaf74ea52d43e160c141ff9963e3c97dbd320a21400ee71be67f8c5962463fd0b4a9056b13b8f3f9debe62d33cc663c94dbb2f4e4432d4de9d9bbe497771bb12a1970d7a1172d49ba1254c185ae488251b9d407441701e4e0321a599f64957b141d898595b0f3e921eaf03a224dd74267f3db594f2a226d24ae2cb7f0ec4da6f2bac1a4160b86bb72c1ee695af2e0afa003f12de6d8bbb4f7da6a976a4e589c084c647562c72d62f63551ea41565bbc7e00fc122bcd8668e5c96eb1dc4ad4eb95a9cc7b24c6a39a0af357cbcf2a896c84af9b8909f3f9c2943f699af0ac0b481602d95ea7f9b8136a87bd6ffeb6bf957b198b72c13c0990c3dc0e07203958c019d1696a128d81940a81c61fd7dca6a70005f4accc66c0e6989fb83896f797e523a42abf45532d6df1882fe068675cea558580e283dfcd2b45369db5de25e59d3f19304fa5f931eb43c363e1f8de5fd9560a691dd96cededed17f28b17f61291c71c13ec96ea9d05a41b18e77351c77ac5ca40c7956942391f0db0617cad6efcc660f32bf294e8e0ec53db4c004a15da72886f8663de81dc394a50df5fb01237ab30b6d3ed35edef334a0b1ef7afe7249917805257ca33dc5be3468a680037b279708aa02bb5cb3a3799fc0f38d979958d81c58634b665fb4dc2a7bf529687bbeda3534f7b3f45049da0a554b36e7c42430c761193ab58cea7377692053e0bf52e7a725feb57fb3b7c851d7070a491ff26ebf034910d81880cd94af6faee459627982ea787697ee759c97960035bf29dd26e3f8def08db8767670661de8f418e7cc61616ac67d905fae62f7ab8024f585b66661fe8ae77340ef676cacb160a2eb714cb706ecec7fb10434d243fc6b7d275bd3a4f9a22197f71f887f01ec778612e1355bf8cae5803a2e0d95746df213facf125e12e4d8ae0436c0b789a24d1b1efa24674a71a9333225edbc9ff3ac5fe16982bc8ddf1c3a1eebd03de0c463fba602765ef0a6c9aa1427d8fa6d49ef693e2066fadfe109da16a42cf86f830663b00862e0f2e80a6be0ecd38e78481be0a4bda391ac640e84e7eddbaf7de64c96d15b1d0f1fb89d3f7a87c7e879d56b783819e85b40f71370aeee3f3b0aff8788dd93a67ceb4bb677471dc52001972fd4ef43d02afcba4fc1d56c148fd14a0d35ae0207f1b9bd9e02c5bd7de1cee58ee3c9f93c57651ec9519f2aa242ee18d655ce3bdee5be1730b53a5f7b8912ca9a4e632a2dd054d98c63b3dbcb077e23ff415f404390d84127dd77d7ebd5dcfb53732f95854af2f66d0fed2d9f8c7cc570df30b9071f549a152ef8d097431bb5cb23165505edad68f644d42e053faf78d12e73dbf2af9a1d3a76ca5b115af901f9ac7c3751ce3ac4431cb83255cd5be3f8670b33c0e71a73682589bc613e9f4d6222523e80edd6a28cd112c2f9b17af8cdd4491a0267dd781a118d0f899c7a4a44d85adc3a385cbd19ca397b1cb57519893546317e2852ba70c9a7bf8d44231cb3fb84edf5dbebfc8028b6f57d0deae801fda2bdc9fe40dcd1a1dd8188d1fb617823dd101ed854efbf3e740910b8da9a59d838c8a0c858cc42883fbfcd1d8dca4efd1fad4a3f1d7b2a3f538933bb7691d9e01afeee846e59264dc0bb3b63082d689e06651c6fdc377606fe7f30a8ad4c0835f15f7da5b6d9bfc15c2340513ca2b067a256e708de8ce134ae05866862f7a1439908e72a95f71282439c1d4c41273b2a46e0120708ad18d11f0a8cd015dc08ad77ee8f9fe6f27cb8b55576aba3cb2f586b1cd336d32c94c9afa618c375cd43f159b9f6918517842025be955350c0d0093d97c080343fd95b9d648dd806f43976f53496b1a34c67c3dd3d6af1f4bb24a46e49eee20e9734188e2335082d77b51cbbd270e84ecf571f6623caa3cc1f21ce81e8339d5029f7ff58c77c69120eda6ceb3e6232186e3c20fe7cbdb0dd58eb0dd1eaeac5873def73cfeae5f0b9da40d691549861b583d390b8c54709f2e60de694c76270c566bfb16b155b3b1d46b859c6e7f25d5afdcb68fffd1d1aa5bfde2e91eb68bffa820cf840c4d7b723c3bb49bc2492c2eeca8b82e0f7f1db330d0799d44458b67fa850f07cb8cc5e79f93d6fa9ffc07cb201c176f3e7e22f0f805b9a0dd92e27c645b4094337ac547e024a674406a554cb75cba7a4e9e0f511fa68d0aca19296a38df39e08f1c2dc09d6ce89033bcf3224dba914d62025ea45b4dd75505bc339cdf55271f7a81bd12305e3b52d41050727403b6420f3c65093a450eb89f027d5908f0a77ca67a426b622007c55f2dcd334c376d3fd5153b53e7c44cfd38eb6ede7eced9e0d85a532dfbf1dbd88813b998d75b7cfb8905c0adc9e7d1517619ebdc87e1dc3747c947990fc5764a8073844c315e7f43800bd231baf384085871257e8eb54f1eac12781a4b8096c73755ed6dc703f8cccb4d7096379a74ccb2c2e9ade1c6459146e34f540d6954c5400a804d9fa39ec0917872a09dd6101743d861e1fe2ca989cb6368eebd6e5c2bbd7368c49be3c48df85cf927c9e4d3b6584684e84dfb903e2a4bda28ba23fd88c2680dd92c8a8a825abcd0737d094b7c4ee6cc8e2257e24883ee5d33c4b96dd62bde773fec035eaae169c942d15746fdd6e3ab4d1cf2aab009a3150730f6f12ced5ff8492115cc5f33c9cb463ffb3d7a95a623132f8139cc1e7d97a5466eeb8df4a138df345125eb1136524b0e7799637ccb9d4f5325dfcaa809a8f5ae64889e474addf804643f116e9592f4d9457c35f60960f77b861e7eb0112ba056d87acf6e48404fac61c92fadc759f5f64d9f7cbf1c94c6629024e832ccec9516be84e9b8c2332a9318d127e6a4746ca718262b3555bd5fd35d4d43a03121ef8223c24f37def7f66cdb5e1d87974e94a56ac434d3a76f4c26e61dd007dcec37adf8ddc8755d7cef9d5c63a7df4b54d46f3a88469f2c83434a381d0b933d76ca4dbde056d30b46f3a2cb6a7c999b7ebe2baf601eabd1e9ca2c419d57e93ce3f83b4a7e1eaaa1847ecee9d00c370f99a326b90ce5b413528a9b8025e464e12fd339a5bcb989bb03eaecac658fa30c2f6195bc1c49e10a86aa6d0ca62d028ee2adf8ec565012ddd135f911e35d0595bdbdc45bb898a12a679ff28a7f9d72c61f9b1f81e57d889c6672d6412f478c704624945348c4f24c55bde3a856c4ffce39d1cf8e4ee196ee384c7e84a560a70f344d3dc0fbfd06afeed1288c7d794b0cd5d0eee952aca30b43898c2f3634d6767fcc8e5736974df038cd65b1d8d026c16031c66dfcea357eb9cb93c701a537ae117f4c96f97356c374ceb205543dfda3e7b5e1e81527d071fa115931b93a80b1668cff3dc1449084511e23d740abff266ef7bcd5130232b105e8573dc875f607b2576ef72cc5204ab39f968a7477e164c0452362aaed06bc7ab9e06d76f0172af10902050dc24e27e13edbf1b4df7893c2dbf5dd0a25ef508d4c14b31417ad260bbdd5e938fb599a7eab1c7a4a823261d0eb1cd358ff536d084f7692de3ecb68af6511176196ff2b1e0545215a52c36f9a6304aa500ec5eda2d4d742ce35f23a1bcc43e6650c990a9f71dfa7e3e452863b4d8ff445095260533ff95ee692fbbc875a5b5fb32f7bb7d4b6afe8365e97e163df51df2c9d84907023f4f2be54696bd9c35ea06de47b832e9229d8899fe6050d5edcd1d386a988667fb9142da808a5bcf15243737b90c9abacc0fecf615f7a9e5764d631cd508ce974fbece83453c76e6f008f1b6929efe04ea953ec2cd93e2ae82f1b153e2eb58c529a3685f4e8897dab4de41a59d17e7a5b154e3dec73f6788b53b09a689420d35666f52464abbc2ff1d87fa0e264a61cde9d2e73680bdc1d9af3646a274e4a5cd688eeac22048eb564f8a2e3877516963f4caf33f8a3d3071614fdec78141d11cd0c3e08ab1905bac461ec681141b6a894304060db37a8fe2149898c876b65faf4e46f88fd2693007e1eb7ebba472b0f44af61c8190f850e498ef6997833ee7c1680d0fee67cd7eb2cc9d68efe462c792fcc777004eb7041c47d97424beb5384349ad665f844ba20ab96531585d08f390b337d3ca3884f147b57df39bb7844a1debbe22ba4a7c614f509b5fa92c4152f8ad8e955d25da4d5b78d1e2106d4bf668f732691ee85b0f1b15a7a163f5738d5a3a2bcb97a517152643837f3295072ca8873b4f9db532ddd86c7a84619b92a9dd544f8e10415c621eb5b7b5f435fbe6e9f12657cf2e2705f8160f2fd334ab9bb06aeb893e377a4ac994a5984da42a4cc968fe90298f04faf8b93540b4db7397bde786947443db615e89d0b244e1f3c29949f7f62b45ba6e975ff2dbe04c2d32ff82ebc87ab6c6c16d596fb16b8f573cd9da8ffab64f3ca4d5420e62f2f9bff0f0740f8bf7724907e7bc2abe0bc2d7fe96bd56f2f6af76d3d33e9df4ae7bb04aba7e534a00f56ffa55fa73f7a8e55fff464c810d35cf47c1c7aafbdbdce03ceab9c3b3c79017ffa3efc9e0b8e9f82fee124c8b9e5b9efa0913fb2cb89959c2f4d2b15de54eb4f0666fd28fff495939cef9fc64fe54d5f6a164636000040004944415417f3adef7fdbd898febbdd6b6bff849a4cfa6c9f516fc05dd15667bea72347e02d6f79cb2947aeed35f76504381b7aea11d88ac075d77deaecb5d9f85206ec4732c6661075342ef0310ccc8eee0cbe82c7d5d913c759c7d50ccee6a70ec5a661d0575f46d9d1fb67d3d1cf4c5766d205e86bfc50916bcf1b3e770a5db08e2f0759c0f22d8cd36f43e32363a7a1a36817506ba5365a2dc014ce5c23346b49d01440171efdc15b851b834b34e4113c0efc015a4adbb822a7ce4d5a48a3143759ce85277c5e58067af40f370d4608191fc0b57a602d5e04a40680e34ac0a1b2fa937f0a9904560aced0cddb88da5a3ac455081e933c001b4f918316b5342ecdcfd416148549c6c8e4f12a706b1ded0cd94d5aceb67c28d02dd95b2377159370412bf74bc67a0f4468b90329bbc6841c6d666f0633ceda6b4d5e97c7e478518e63a9091b1b3d81c76dfe235c34db022d316d8a8da1f9fc0f71547e90adf7a84bb065a52b16bc0b50df608f1cb18a6164d14336eda32d6556c310e300dbf05aa7a1faf8cd027e9fc3f31cbf042077a97aa543e60847a39386e52ba752dbc07963752fb8bea73eab7aef6c7e27b07e677574fe7b2702cca43f13cdef6196650fbdafce357aac00dd332d7d3c03825d7ce8a3f65d3ed59f9b5f94fd16cace4f3ff6974bedce8e33d5dfd1e53756e9ef6c10ae31c57c5952537dab665d0a658373def3585a8d2b356e6051ed1291738c8047020f68a3e119f3c9f83ff16dcfb51bbb377ef6a4e9f46fc96a12980f9fbc45a6a81db0b7381c69cf43e2fb7ff5577ff5d56f7ef39bcf38124fa7df3711f094eca9472011b8f6ba6bcf5dcca7ef63e07b8803b3c0c824a0114f65d69791d131d5d1d171dc8f840ca1e4338042ca586f9d7ce88a8c3ac6e38fcca6b31fb726a326ab0c0ba04b6910bded051fe38380d1f3c1cfaf67e6798f178b1ad5358a6e7601925e252409e09717029919c219f1e523577e980f6ff915b08b6fa54ba76dfbe0b33348560c5704b332e68294ab0b65b4fa9736535f5f34902974597a8d1b8d088ff14087972bff33438bbd842bb132579c01f3b20dacb2072ba23bbe28aea4ed576cd0235f1831d08e896bef6d95f25e20eb6fa07980490963f4a8c11932b7104a6176159ba2254ae9044379e0cf05987cf986b833d2b180d5a63f0437c371417b7953adb7df44bf72e8d25fbf0131ae7167595b9c5af358a68d6c5bfccda6619a1a02d9966aa93a7c36df63ed2e9f020a05bb9b352a52b7b56fc7208d6a0729c70219ec65c63e1aeb78360515c3b214ed09dfe21c22fe46d6a94ff7f1e346c3847a195bd9e6976286f22a305f61b957b22e79b9bd652f1a5e9d496fa0fd5e71a82bbd531100a45fc058fe1fe8e5bc5ed7f3309daece2506ad0c7196a4b3f75b24cf554b3596b0a72eb59c2bb926506e675f983d83233f9c4739414a46b27dbd9e57f15c2dd9524825a78867b67f5b6f11439671362381ba90e1f9f128ca8d846307a2dacc383d5a7cf564be78e17c7d7ac5eedd1bbfc1b31a4f823b835b03ec94932867798c05eb8adab72d029ebbc4f557f8d2eefd6f7ad31bcf6bf4bebfef23900e7cdf9bed161f6811f8cc673e7301b3c8ef6440dcd34096b02533e619401d0d1df1ec320e9c35a8da8e0079c75087582e001945c90700318aca5bb4d155cc04fe9823a29fcca2cf9d456fc03c73e8d4d472f4e97cead7983fcb35e10575cf80cefc3b28975dea0b197be1885356f0d1c74284f1ab666e18e1e12b87f0957cf4c8d1409cedb3bde8aa36c642950762ad79f702010734e3a41e41ad2c292b2d901f6e767255a3523f12c1616f393740b2626a1225c6d75925283a41526f256900d6ecaa8dca69ab2e54d526dbee9f9a6cb3b9b45539e44dd655fb2dd471ca4cb8d5886952bbcd8f55fe5c18a9cb370f837386434bcd9616f28f114d5a97780cfc6aceab1e9d35b74abb64b214c80242b5b4873df978a84a786c43d82dab37fff2f32951940dbc94f32d0a7215099811883deda64b58ae6f3c9620da3a75a9dc4c0a74147db1b1ec6bee2fea34a7d6fc2d9d830d661b9f360cb584014f6cb4d5ee0523b1353967733c7d8d7427d457c1fa2a40b77e7b6a40baed1ffad0876eb64fa3dd9d7db3d71e1c6de5d5fd7690be5ad7f3f75f042efca997bd9a9ef686806b7bb4e7909d3427817b7db3037a6e39825945fff67c329f7e1c62f5d394abb7971c7583bc2794635ce9528f356e7cdec4bcf6a1a85836993da7d8f96d5a0953e9095192f1252a42620cb5ce13d77698554805facc0ea24b63be938a0f00d82fd9bd7bf7f79c74d2497e5b15d7b68376454c1db0571cda96d7aede38994ccf23be6f78f39bdef42b8ddef7f76d04ecf63d9de011b8eebaeb5eccd7ee173910661a3003e43000b67c4641aa2917842cb092e1d51157fc220fd98c970ef8b24b92b6585c0b10fa67fc96e89745e9f3f9d65a74c07bd615f2d0103ffa52601dfed37870ef17107f826ea9ad8135f54a099d42808ec461900f88638648d7250750cbeda0ce95a20012355e35d45c55458f1c1711f53a3b1f8e342bea078d0315d918284379e388e5d0b828a1383731d8364e552f54546f22c7b6a2e96b2c63941ae5db97c3e543f9b9b48d5f998d570f802f3a8646685af98abb257c5cd2889917cbba3222476d9a288f7fb663886b9a441ee15c3c6d0b536e43488adb3a95f02067c0355498ab6e78004c52da8bbca6b8ee6302a2625589556424f1499ce830890e02033907207cc3f1a9065a2f6f310ac3cb418c94e3b16368a25b65e8cec3b903ad7c4187201d4d31c846992a23a41c29e2ec9bbff6a3169f80fd812fc2ea182422973a6ca8582113bb1683bc39294ae4493c5e3b5b2c5ebf9b874adb0a18d7a9b707495dfaa28ab6965c007e34002defbd9d9c81d3c64e60bdd1ee6d1fbafec323f08a57bce2e77890f92576f9a4a1fb99af3e4ac6730e7afa36f99c57357b012db7d8d4739ed947edb8433fae6fa6ecd39cdb92559af3d7b3bd2c3a3654cde041ce1b6cb0af5f3e95d3ba4847cc9b047d0b155b9984d0c1e4e5e36680ca2dffb1a1dee0f0a2aa3313472a49ddf87ac6d35f5e5bdbb8981febfa0a54938e2601d2d37f07107fc8c3a88de744ddbfe5577ff5022e1b6f60687a3fc0fde9e79f7ffe2d276a2cee8f767b06f574024740900e32b8c861b18d59e2b80cd48e8b6d2824efc02797e3a540c9ced306460b0eda32848f8136b5ece0ff1c3a9f371eaf03d2377604e9302fd36c34fb1ade50f77f032a79685405540dfb0cdd96f525170b2f101992c9c4f1b03b8ae78fba61b8d7e5d2e55e2e2e4402d7d6a8dc086807c6b4917ce210db685b96a3609093df7f7ff0077e8b8983fa899eb2d4f990d56832a50e8dd6ab193b052e61359804a9f9a8e5763ce4ae4f81f25ca46c1dfc99718e7f30850f496d28a03f3ae45e7d64b5a7ddcc4a872535699b5cca6959b098f65088bb861654ee9ff5a65c68d9d7da536219136e5012ec6cfb059e2ea1196c06c847ba0c994de7413386f2da460df0b11d9979333621555b5299065a96319ec7be8d4c6bd5214ffd179b6d48dbd9e3476e32f42b748d20047f4584ac454d24557ff7382391ca160bfb857dc77341f6f40165a2ae711907f5a19d7de2632672788c8ef6061ab87cbee385d3f1f87f6d205d75773509a0dbe7aeeab8a3723b01f29d6877545fe7bb7b1160b9cbabe9712fa94e970e68b7b48b659fbe6b59227d30e784fd0f42f5e9f4687a360c9c22192fe19bf8fc91a701e7b9674e7dcb58bc35b6702ec0204f6c7b52a8c3f34de3e4dbf86249941f7d146a9c9585026347c69f12b172f9c979c4c95b676654c78f9cb783bdf80b63dab2183d8a31e6a2d9fce075bbf7ec7e2d37b67f1b2b4e12d984cca6ef34a3be134dfe13299dffdce7be91c3f6768ecd5319bcfeebc5175ffc9813a9fdf7775bedfe3d9da011f82c209d99968b32a6190332196403b218f82439b89a0edd5565d550575c053c6b1c15202a02ed6646f36701bcaf5f9d459f52a8052ea5a466d3c3ff8dc0da7f438e6ffd814500aa36b0eb10c524b50b1c33c40ee02fb4581d66801cb10386dc973f82cb4c0ca9250d2cdf6b66b5485695ff0ef005cc520f7f4464d0bef1cabe74c09c0b971e87a259382b2ec959d8928b9a01f8e5aa230cd402497652d9e380d0d6058b33eb3582c64567642adb5e30232230a66ef020f5c62c7a64e0aae72b0615c5fdf22ff19343ebce38eb93d74e6deaaf6a14208fbc17bd39bf40e24533b16cca04b1b0c55613615f4463a905cbe8a02dcbd7286a4d67b267972cb6d01faf144a0329f11f7fe261e92c7174a4b2fa48818552a5def8250b892f21ca91f84dbed9b372e019b495fd148a52fe7884a3558992d12fa8997594943232b6d5b859360f9733faf6d0d2422c897f00059424994944683fc7e007f66f6e7e300436ceaafb50a9cb609c553fd28c7a9bdd56ae01e5b69776b424dfd1ea5bddaa0d694d7fdbafd29a4cdfdf771178c5cb5f7e213dea5535d55d7dca5e973e19375222679d7dd02c2787e383e8d8f3ce7325dd9272fdcb54c42068b2f0c9bfd5a3eddde888c298430d32f4ebfa164a452a558fb7b7704bf27cd73de9646ab6bdfcf2dc4962a7ae58cbec861ae220f2c3844054ab8b8f0af583732e0fb7c662f1a3693fe4df994e673ffd95afecbf8a2a5bec679900e97d967d88c65bdffad6ddb3d9e6478927df728f79c5f1e2e9cf79ce732e5b06ab67eeb508781af6740246e0bacf5ef762be5abfc8b1ce61af0dde192497f1b0e45f0d76e173e6c33f87334639919a436806ceecd5a502e88bc55e40c90f012f96207d3a9daf09d2e530b93abd817406f1b3e1af8746a1ab268375dc2bf0edb606667ca8511de30ceaf86572b0765c161c65f497ec8c6e395583b71a741a9a6c56d537036533171374ca12606a8cda45075ae8e88c0acb8257f7b9102dbd2b9a36acf74236e8d05f536ce722a2b09f5adba936ffe2b5483895ec5cd7acb3a965c73e17c790d28880f4d28f0674b61f6f0ae8a51c336ea9d47ee2a4a6a1bdca1a73dccd45353c65a562a62dca99cdc7b51c95047305a4a3bbec78fc22a0b278ee8033f16930aeccb11175fa33e825536d9454a01623e47528a6b369c7de0b787a60ea4ad62f566c5fda5fdc4a6332464a8f7ec56ff70a5b5732663dbaf60001840239b6b981e19b13f42bdad4c9ef27377510e38a1b88c6b74e15747813657cd56b7592745a8cbcf694895bd0b0b90bfb6f029c7ffdc03ce23707d66fe357465b99e73c7821d2d60f11357a03db0d34afeee559addf5e6eb3efb7b7dfaea3d96e7b7fe8683b986f757d7fef4640904e577a955d6aab3f964d7b99fd30fd93427aac639414efea3d312d791eb9f722917cf57b35da5755910f2c753ec8a4ac5bf7f5c99b9ae4b54f4b8bedc69b8a7096e020c9f81a2b9c4f8e84ba10208f78ce95f83898a0bec6d6762d828e5afec350631c0a38e992b74ed778452fdf487e272f50b88cd9f537b286fd0c889e5b87cdb20bd89d596f7ba54fb4e42f0f13c1efe488dc441c79c5f1e803175ffca6f38cc3f6b1607bf9448bd53ddd5ecf969e4eb00838933e9ab9dc85b1ab21868cb8158840142bfdd470996c00ac3907dc817f097019f932c432080a6e7848703fd91f03847f82993ff138b67838f42059fedb6cba4513facee622f10b28d9e500ed5f602b4067d0ac2b30d600af6b79c776fc00d2da0e077579c26839433d7cfa8b5cbe5e858172b4982d7545cb4541f946541979f9d4a14a81186d9d04745955365adbbd7e2436f066090cfc599a830e81b75b54c527ad90895e842c916c837c836d1c0c07be07f46220b5c38d49417bd9e15271d408f6e083163df20e7154ab3c39c625864b1064819af782cb13455ab6bd946c3f3aa3515de854fd963e5a83af895288d4c19fb255caca6f1d1f8f89595b97808413797955cc7f18b42b7858a15573ca3775d61b22b445c20767ac0d61be0581d8c072a974bb65c33ea4605a16072baf2ee5a2157acaf6058fa2746de093224a980216a24b59eaf03bed099376e0f3e0a94d85e850562bf55724b5d9e4a4c5620fab0cdeb66b3c7ed8400958f7974739b702d21b58bff5d65b7530e9ee02e4ed17da9dcada907ee47dcd46369ffafebe89c08517beecc58c3dafb26fa51fbaaf8e5afd0e371c2a92b2678cf27ca16fdb87ab8e8a8065b8721e147bce4db2d1179d43ff45de73429b6e73ba7a8e50ce3fbc9e2f9e93d699bc71b5ce53c2eb48d31d97ca21abe1f77c0b6b78eadc831e4bb66d18c2c8d4b0084d9d141cb7d22ef469577fe2531c1c748e01eca3f9f7f32bad573c68cf9e9f79080971cfa543003be5a455b02e786ff41361cf0cfa751cc3677138fcc1a95dfcb237ab602efeb9db1b6fb68f1f2742aceec936da9f7b3a8122e04c3a03dd458e710e5801773552160071207350a467380bed90e7e05a1da50dbcec33dac24872bcb546ae80a6d1623f83ee4f41fe7f1b487726bde6cf81e8fe175e0774f800e9881f7c19fd2b2c72e2d7e099891d88f96f448b184b1d79dd8ea3e628eba3b41ac82931d406f4460ded90217cced42a3ab4c706e7020243109e4a6485ee3a091b6892cdbf28720699e8280bf39c0b811789d8e74ec535d9f2ab1726f68371e5d53d63e3a520b6d9f1571c430e0715f738b876d97cd8a533a53b9e11070548757c28d89e342c442aea58862e492dd8f382183f154e8ead06f04057d1cc9e0cc6633975d693a0eb299a8b0faf5c4a537545552e6d51ce78a60da1449fed10ecea8e957521d657759776ed541dbee05ccd95e9bb626c725350adb0cdc3bd467494257dd9e2f75815a0d7a6cd88717270ab06a7a2175dc63cb6d96a59cc509628db36da1411e33b1cdbb616bffc561e6d2a4a60f583bc36ea8e32b6a062260d929d4f3664aaff28139fa0701e5d42f53f639fafe2b73f58bad3129876f16c601a35474d8def9eda1fd558afbc5722c09af417d3432f4adf4fef19fa18fdd0fe96be9d2e379c33e9c39eafd443afe74d708d42be096c5e3641f7e15309791334c97675cf6dcf4d797233ef2055e839fc51131d3268b77468bfe9c918869abca9295f8fa97450037b9955cef32a35c8d649dc261424e7fcd17cce31271162a264cac5b259a6e3731a321adf8c9e5f64cc7803cf9cb6c74434e027a981f5d57dab3b11f6175ffce61713b68b08603577317a3bef5d7fce916fda0fbfa93f11e2744fb57188f23da5aeeb792047e0b38074c6df8b02643dc1f80fc863c074107318aa77a7337c42c80380c544258430c958fc05e3c84b1f06ca00b3d1e417f88a9e5f1e754d6d03e8960e0eef74714afde068c65b5e003a4fc3d6abec88b990b0778d74cab988e894b29880d18b41c6dd8cf848a47ab808c50fd9d534f09a8bbc63ac796b93454ff108f252dbf886af5d150c18568175718a0d0ec406b4ccae0efaf2461934e9da80cf22a7df5e5d84770188d47bedf282238e95a966636d8cfacbcf25c88d6179bdb82ef832c4f9a7e28d27fae6477dfee9afc5f8cfb18c0d552b93aaf2cf82bc5c580386f5d318f1a77e97cd44639cac38a6bf10b0d840d699b53c049998957e9b602a9de6ca684c51d962a09de89797ece066fc8e8e886dc9969ebab4d7bdc120cf2ecb5134c501e3a8d90cd8add75e7612f8345beab18da1143bd906bcd5a21acf81f48f743ce7ff25c2e7851e27632d3ad8349b2997a1c6d3f6def82cdbba22174d71c6a8a0281d08ed32434f8f5e2c5e43ecde747b60dd5732daaa9d2e9ad2ef4abaa3e0fdaee8ee32f74c0478bbcb0b18245e976ee520e479ebf946d6ae6997cec673d62c497af562fa18f4f00d1d5415cbc12c754d579380411efaa7ebbf9b56fb7a81f39ae1b6efca56281e913864bf861e238ef7ea8eb2819b7c84f4411b6c38e774cd72aa1a8d529dc7d0e5911de69a6ca00045dfca0ff20cbee1d75eda55be8413d260c2aa1b68cbcf732dbbf8d4db6ecb5b966eccd9ef085049a06e8efd72594ca30d2cc7edeead6f79d31f72e49ee83134948cc94bb0de1addc78d1689bbb7b74ff7740244e0b39ffd2c6f7799e70ed841aa01004f3047a62a3bd88a116ae074546c93a50e80e173502e2e462b0743c7ac1aa205480ccdef9dcc66af70690bb37e7e75382429c33a17720c7e9b808eef9e4ec72f0293ecaa9153dd986100ae8154c7329652264341f0185f29e5862317956a44798617ca0c820ecf8a0a7087c9cce82c5d35c0c8a0fec8a32aa6a2449bc683bd17057464f0d74bfe59e3cf6e1050be5802ee75ab34a2807c66a90591560c3e0d5eaa36746b2a213953f752832ab6da2b1361cf4d44351605da0f57ea9a8dba49803454a931cac28fc3837c01492fec1a926c5b631492f9765b8657cd66dc9009116888443c040b01b3128718b95369ddfd24e6c62f6018218fcfb23f6e698a8f5946a44e52fa59fca992c74e3dd6369f6d44b1eb576ad06d1e2748fa9163892fbad36ea64ac63e4d0a7f5df04b0c3d6634c2bf9a8c9300c3f93a1f0a8d98742b2d864f1eb2964ba8fc20afd75af3c6c8b3a71d286faaf2ae75eafdcb0dda7cccd7cda3ff8df6b89cec8833ebabaf6abcb3b3eae8efe9188cc02b5ef17240fae475d5afab01d5dddcd2ade85b192fe94b39c9865d005631526fe7240dfd3237e196a3823e9e93d3a227ea00847387ac32cf41ce023a798165c709d99a4eed232b5fce21e8f2a63a06738e682e9318c839af50f59e3faef2a2151290cf788ebdcc93c7366fd4d22fae0f39cf2358a342b6b97e956dfdcbb9a8ad3456fb7e74901d74389319dcbf06ca2b0f1e9cfff6a9a79e0a60bf7174e38d1acb875d80fa216bd84f04b0feb6b7bded1be6f3cdcb58569ae5aac692b8bd85f5feff2241d961734f4e1eeca0feb825d97d7b3ace2370fdf5d79fc757ee79bb8b337a198632363924a508c901ceb1c7916aa0537270ce60153186bc651d3c02fa86e0e125cf80367ad5e1201dea3231930e489f8e17cfe0155f2f83ccafe40d36d1951fc08118b5ec337339c83a4be260ea80e0989a619641bb5c1a7cd6dfaa2a30046f067e9ab6d50e84d35e18b15dd727db5fe02e20497536515bce164509e546f722e3a71cb1f15a0de8ac5c1c0c7f720ac22ba85bce5c2900cda8470d8c0e7582b5bac055bc33fc0db22ad427756ac75898eab878054ab11c4d95768bd70b64d6cb9748e2a2dbb1cd066b2a8acf95478df6f8e8a331533dc5da5388aad8c476fed50153ae86eea36929135b488745e3247d2a8c5bedf298e463a579ade59ff8a54e7ef41affe14f567527be38929e1c82157163680335c8d947b66efab4a1ac74218ded8c8752e37bb582223cf9a35a293fad7f2596c8299b3e2abb7caa9293bdc7405a1e7e86a4bcba23238bfca1e2a1fe27d531a0afed02a0fc12317830ebd3b336967369ddf5eab2b966ddf5ea070e1c98b4f5eace6859d72e90e67b3abe22f0ca575e78013de575cbfe621fb22fd993e843f52d4e08d5975bb782c99beef449fb65c2920e983e6937acbe48dfa24e3d11a59346b712eaa02ed276743b30a9ddcceb4771b04d7f46322c54b8977fd8aba3e995b55459e9d9e4b4105e4877a70f3079def8d7c6a7cc960fe83ae383fec94b3b95cf75c5724cb3b56165884cb962a5a4e5d83e1a9d49e93f6cacad7d60efdebdffe04b5f3a697afae9a3c9e9a79f7ec2ae61775c79f6b39ffd475c777e3907856b438ec16871fe57f67ee57509e60e9b361e59b53a26add277103be149f6d79e8ee308fcb7ebae3b9781ea9dc00f7e95cd41b946c0cc9872f41de81c6c1d209318a16a36553e288caa0eb0720490649a049a7aac7680cfa83ada3b9f2dbe97a52e37e6a1d1d4eebc6136f099d4bc405f32683b2a3290c6352cc5a7b8450e33f2e89f60ce7dead952936d7e9e5e1edb1139edd62dc9202c2b59a2c0def6155d9b356857bd3aa96257764bbf754b5ff59340683371d1a65e4528667532499a1aa38b4c5da8d4af9f12cccb0c1f65ef9372514581705147a22adf5ae8f7d07a78956b36c74c45cb3f681af6bae0d0a99efcc34fd9f6425222cea9475f6c572475a7fc63177e1dab57083a18979f88c15dde12cfbdfc60cf9760bf99ea2f22752bf95b60f9223eee6599ff1adfbc6c10b38ddc78cd16ebf4495ea83f3a85e09ccc60ff708eee5771977332c7283f6f3e1c3d6cd48d8ceea6f53aaf9f890d75892365fcaf76e934a99a8a7adb47c114a7adf0ea6c7be5a53e3b9f31a07daa4fbcb9a85351dad86a521fb097f3434268f0986513ffb4a7712b070be6abae22dc2cc770f8e534594fce7f1a93fe830f9e7799f5770f1737d4efe5d5963fc279745bc4d8b435ebd0963f84b4d3cc7ae3effbe32302ac49bf005cfa86f49cf69594e31c0834e30c9d296349fa966db6d3da676b1cb08f17f2754f353b4fa4f457b69e5395c853a79d02b05448c88ef3c9bc05b7e46bd95ce9890efbb9bc7c6232fc751ecae5bf1aa4c84471b0954cc92153e733ad6b8ae094d7f1821b55b6eca30bba0629471fbbd80f37bef84c51f2d2e585157625f2969a5ceca0ab4b15282ddb16166f81e7426e88bf5233ecccb16fdc381f5d97d3130dc7f70cbba0ba7d53c72b1b4f25685710a4476dc5caa88f5f7bf24927ff449b3038d28fb135b0beba4f00fbe69008d82f7b3a4e2300483f7b3e99bc8f61694f0dd605e71c7872c9f7cc12b764786ac339e3500627075b0627386bbc4388cc8071a05b1f4535c88d462f3a38997d70ea9b5d8e92168bd9b3117ebee3e7f2e3c0ae21c12fbb64b3d7261952400b3579c55d687039ba8ade934a563ebf16b54d052ee183165797804db5f2d3781a0bc88c7e81b0339d793f7bc65c7850afb6da3a589347c619a3806bed409a80e2c26543b0af6b2927638b528a9dc4d132ba8c70e2085f8e4e0c42db2697a31300593e945fa8f5a28caa84214d45670e6079acef365bebe554d9d3a380bf217ea9b70dcae6821f19975a7c0ec7fe929aeb68daf55c486fc1df2fa2f3562ec65f1a4d79a57756446b805fcaacdd1dd8c229f36a6fa17c603e3f95e5500fa7e60c9cf95afcfa5a62fd77706b8f48396f69315238a3cfedd8a4ef8817a4e5d8784cac954bdeb490ad10a6e251372ee67314a2d3a3d81e82f350e63b04f4d50f31a185602b9f197d75c6c6968ea60bc0bc040eb83bc41ef9f48f88215d3ea9233ee64062947dfa0474dba3b8ac3934a1d99ef94bd0f51bd8c91218598e06d6db8575f522ab4c4fc76e0458937e1efde062fb8860db3ed36e2eedd1f6653b8d37f2e976e96e752eb46f42ed83f2fae77f3a9a3b2921c18f9c5c99a88666d9ca82c54a86503c8e9f1605b6aa7190f18419787444203fd40e365517ee255b2cb66fe5e466c029a9e1ec8d33a855d7d246cecc9cc72a6ee203e6965947382fd93b9662535f03c2532349da60cf7d645674c54fda385f5c3f9e4c5fc82f05bf4fd1a30176ebdb1a76f3c7435a1d47def2b6b73c9d6f45df55d71d023a1c1b6ea67e7a365bbc667b7bb783f6ed63d3aaeeedb2276a79383b4ed4e61fbfed66b9cbe319803ec4c07d5a1b8c3370674c75a0e23f8395f30a124da9ac81d68131482e43745515e2a01e8da8c84581818b81f2edccb9fe52e938ca76317b2ea2e77b420778ba6d83ec0012055976caa54764aa1cb413df1c2b33c0e263fbe11f79aa4dec2c2cd1cd70e1b0de843ee3a1d6622bd0e7805f752a974f7036803208569bb45b05077475a387ca2297adc65254b7f14ee9c8a84bdd7551454f9a563602d035848ccf0a4471146acfa8492fbfd4bcea4f0af0ca5ec7d6bdb6a598d786479b7d9a099df87b08a8fc1c359fa6ead3f05d4f643fc588fb57d3c522b3b662ea4aabe87aa0b25ba52ea673573f5522b75568c4b6b7c6e71676e058910380ae8f67b3472ca6e3c713826fc2dd2711bf87a42d3441f7734ca8a46575fc2b30a9ab894618a1198e1c333a48cdfaa5a394920a137998bc818962bb92b29a3076f00ff410099ea66a6994b51ca162679f2364253c6841afb4c8a964a0e50090f73defaa56caff2167466090bd1bfb1b2ffdd9cb79f4748ed5554702ebed4d30ab0f972adf2f8446e1d84e82743acbc5192f5b87a349cbde65174a3fad73be6aaa77d9bfd36795b3cfd98741b3514359252e3d57813a0a1ac30783e757d49a0b2b658d0e723e4b52fcd6972e2a13ece0e3e4e4f7df7a8d35be212b3b867333c1795140df6fb8a0c74f850719b3a5091119aaacdf9eaf0a951fe471acc68ca255db8a27e3e420a3ea720d7d9cbbb6c7723eb5618b0e6ae0f8f5b5cdcd97de3a9bddd4c0baec7e9bc5af7e6770595daf7e3c00f6367eacee6fdd7bebbb69f6d33c2eb981cbf12364e3c5b36ebbedc07b8c494bc6c6fc2a60df0ed61b6fdf5704ec7e3d1d6711f8cbbffccbd3d7d7d7fe00a0f7a80c4c0e600e889e3c0c2d357e9923859c5c06c29c410e6e5695486626e74ea54314625b1bd057f27f02707c9ed9a325c0c4f731f8ff7059823340088518c92c6e1c31cf802cb28a31b8bd88e04fec15fbd24c6834666b6943f95cb2e8188c556b04e4822ec9025ddb2f584ba362a72848275ef2595f3a1dec0396608f62e5accc4ebf2926785e4b06d065db60505f7e85d33db4b253f25141366988491ee6553674373a5ebadcc6a85bec181b6777f565e00c7ff49a0b3b1b5594ec7e88d7227d15df6e7c9a767d6ab439bf76319d02c845dc4e716f067893396222f63ba0eb55f60169afe0f09d05a4ae3045c55056c5f62aea05ee04f47f9a8d174f01649c439fe466d494c6d2bc8a45281e37c889957172c68f8a5cdc3d4e25185a0318adbef478b42a8ec6373dc82b39c4dc6c79cc87c06bc7afd4d367651dfcd06c407b0ea847419d6e4b50fd49d1658d8a06a5e6730ec8410dc5d4e4d88fff84f2d3a9981d0dacefb404a65d6463b76f8ea9085cf8ca0b9f399e2d2ee6dcdd953e67ff4aafa147a54fb3cf704d7fb1efd887e449471c9a4a3e37979194666fac9e999bcde8941c308aac1c43e7a3ff5b86407f6413b681603e592b29d465a3fab353dbdad1a7c1bff804506ffd5a9df9341d250196afbe9f9b5d75c4ec609bba8ccfca4491f5dae7230b748b66f5a7545332064cf6d4b9d618e0319b3a273186f314bda5832b873aa2a474a3f3739cdf2fdcb7efe06f6ba201f655b02ebd01f606d6dbdeba632db5f1c3bdbef31acbbf399e8eff9499f53d5e935c3ec481a666bc9758fd2f7cf3f0f1ed6d5c1d97acdb0ed69b8ded722762397df7446cf8f1dae6cf7ce6332c1d58ff0863dfe3ea84a1a51e65c0af5fdf67462f03a6b40cbd1980b24c2423d400681da83220392239303960e55e39e56140bc8901f19ff240db978bb8f3169bdf86e82ba3093d19e4620bfe8694868158571d5f0b98565e77b52f1db6c83b60eb4db02db40ca8d10d937e5331b42e425e7c44d2ca9b327b99a9d6d25783b9be69bc8c9406b75e484a303bc11e6f65890f5e18d5dd142bca9fb6bd1d702655ffb3544217f8cb96d82b93b82aa34ec635ada7ca310ecd36ba6e546a3fa15d757331b45db62136e5a2faa5a940fba3bd485c49e6131c872b683700bd66c9c317483eac3fd904a8af64ad5f13900396799be66170bae477da0ee0db5d9b2d1f7440d8dc017befa4644bd6dcc1e6d9e1acdc087d1317d6efa7ad4f4c008d47d88c1f3120b6f6fbd6d59cc0cba1cf3133e2f2d7319654d29c2bed22ce3e37aa6a8521af9d84df8b7980b97bf4b38d9ed65fbcb07b1ca84adf2d9fa00dc7be6c79b3a52e7806463c4d33dcaab1f5f32852a9bc6ef8d08e17710efe268525586f4b60a0513f9f39b3be7a516c1744eb7b3ab622c04cfab98c29efa4c7ecb293a4cfd084743536f6737b627a087d7f6bd2419eea81d6a76f5a1efa53015bcbc6c37a32f647f6d146d625238e2a61a1cf970e2dc90635e70665666418828bced6fe6d496af1660bcda4fee2895e299ea0b0a833fd3c7979d5aa44230c12d9a555559f72ab735f75a58d98442b7b0766da51e6d4e9854315832e44bdca24b9f39ae2cb6790b1f1c63eb5e4d3c4f9e83dc4e7a580d21b94391a603f5e40ba634903eb3c68fba384f0a2a1db18824a8bf10db3b5d93f5aec5b7c5e42fba66fa8cdb70fdb67d73b486fd1a9fdd09b0f25f6d2b119811b6eb8610f57ebffc248f24407a300091180c931896127c316c0c33b5e814b0de40e943251ef40af48188701549ad545249fd27e06b11f994d667fd2b05858b66f58aac02fc6f08ba3135ec1e8308f1f116f5d4f3055f9325b9782905a15fbcc4a9713e5b35e2cc16e79a69e90b89c2c67ca9bef836c05c276118da69fba569d5973475f1d80a85dfdb5be00540df3b962d91833f2f21f206853d24025f007245597a2e2ada15d856914ac1e2765ac57b47c2bfb10e3864ae1a7406d1d237356d360db520a527f3371b982da2b68c61f8f26b36ba80c5e0e6e4e61fbc6852b2667c985d1431ad0f91d04e94d4993aefd8ab0f6d53e9f4ec9d6d2eae5026b324ecd586e7bb2cbc443cae51892ead921718b313f933eff5ceace31a8f6f7659f6b37a9862ac7d498b6780301a0a7cf25c2a960433ca1a73b34ba5a8b919c201b363b5d715b9b638836c4b5efbfe75be5bda19412096875bcad6bc757f7d03be8c91155b9fd2288c2e3ce99871a75d15f6f22fb2d809a5be1ba4360bd01f57e3134b0c74ee2c1d16f6066f9126efaf7e8b5dda17a8ddda3c613e995b5ffa7ab4baabee81e62f523fb350594641c9185b2e3937d33fd9a9240d5f125e7004479ecd70dc8cb27bf2037f2f22c69e88913057733e65a8f32fdf6bcc9faf018548f820a98ca8f381c855144bfc74af8f10d45b9c98d4fdad2769d5bea32575bdb630b545b2d545b9df3da8ce784827dc2a81e998d47e98c3f190ca84038933472848f6ff9e0f3aca5413771217819e3feaff3b0e96cc4ef099fbae92b1d775e0ef3ffb3f736d0ba26559ddffb713e6e5f9a863474139d3590e022a66596316be25a09a364a14151464671a20ed36da6ed4c10c380ba5034c69eee56065b647034114703082d0a8cb000653e681827b8e889836131b3d2b62e96cbb603fd019de6fb7e9c73def7cdeff7df55cf79ceb9e75e1aec8f7bcf3d75cefb54d5aebd77edaadab56b3ff5d6f3bcdd61efb1781752e836c4f8539ffad4dab1639b1fa49bbeaefacf96d87f8ec3f243f3b58d6f3f79f2a4cf3d258c1df6f1468285fd06601c17d5c5796d6a767136feb0b5fa63777fecf7b053cfededf29885b683591247c164339105b43c1ad00c92c5c03478dd91d570d68e9f70d0c1d7b980ef3fe161bddf8af3850ba53fb63ff060e6d3c0fb356c1f0f02f65219f89f0b40534c648f0c68f5acdf321708ada66580736484d8a2a235530e90b246b6828050ce71dc9dd014aebce28883d7180d86366d0a9c92c62cbbb0761079aa489f4850f2096ff5b306f88b93d93952746f84588462b87705abce13dafb37bcccf72238d3d8b0f022aedd227f71d24516f0b1af9464a5a336e535592b8e414cff3dc03bf9b4301a149219abbd2523844ed3e351514b36fa9d5149474ecce274b6a28637b8e56778e4a3923d3c1f6c86ba873b0c8e4e7d15df1cfc7d06edd92e11e951bbcc4ea4dfd42a976df5a642c519d7e0d1b7f96ff8d0b9ce8b1512c121ac448d3b63e5821495add20c53b4405d51cb2b784397f9e5f8299f7211225e4b370122b338f5b07208839bef7142667b96afa345fc46c24487e00c679dbec9aeba84e3f3ea7d91157e14ceef1ec0917bea72b1bc8d79fea4d875754d65f4461d0d50b7b42b2a686dd0d81e4a40692e74ce9d6773a63bc1e0bad9513eac74bef5084756836bb00ef451358b132b8c4c1eb6ce0c12a0a6a676d075fd41b0ceb04036279a1855b199e0c7d1b77ef3e02a7bea8dfe233165b24db9144112191a70c31edeb99100dc6a6edca505a7dd8474becea388cd1ccd0d42d0e4a41ce25b5b9be8d64c7bacb630b8665d01260a72460a1b2a9917d2f657e0d3c9ad2c093fcadcfb980efb61dc5d1fdb0fd3f6c2a953a7be01ad7b7ff5899de7b8d237f6d172fa96adc5b61b297b4277d8cfe5acef21b8083399461761bb0f5d93efbefb633f83317c6e8c11b3a2eca2c687a662d89c27b96a759c3ff640e2368944002b1113ab0ca9108d4f11c4b1d1904e271f58ace1a40fae91b47bc36cb6fc2bd4fb4bd8c4e3bd2ee5309434c4c81683a98c2c1899cd41092098beb9437862abd610864fc5913717b816fbc81e91b5b208e04266ad5857af31bea6e260d942f9a6241260aac5159314b17d61152e10e12bb634f247d47489c9c8e1053c3296fb0e7689a6f35da36f791861f8b3e85855c8dc21a254710380561ef60de555b745d33b58507945d8e45a6a791e901be1f32ee23bf98c421fa01ee77eca7b2a1dea2fe2544bb3ee2ef9ce64bd62203babb5e5f69a1f9c721d733f3d3daaf8a024bc36763f1b2d6dbcb1dad9586df0315ef943587b3e63d8418c85cd668bedfe99aeadfd097dff13ecbebd047db9dbf1b17b6bcce952c6a6fe04daf9a88963c47b24c5715833e2781971a8ed7ec720ba241fb8d1f95d1f8ab3ceb4636e656a5cd5a0f353fcaaae4eab865b4b6ebe9a0c41d4b918ea875f84c7158789b2e948f9d103b24dd643cd57937c0a1fbf981f427fc77a7fbfba05fd7569072db203e151e2bcea019c74f66517ff1215789263ae435cce276aa02e244469a2934b0d91603ee5b4ab97ea78e52d285bae6e9aa6102d52a781a8be8dbe2c6788530797d83b2f523857d43f422eb200a6f3ca7fe93165ee36c494b5b900cdb2ee6841a380e2d84a78e66637f353399c538d314299b4dc2338ddd1b61e31424722fdd2eb874619290dad0e23ffb9614923c984271809b629b2102b6f9bf0e59493b79fc24ec46a4bfa01b86079db5785327df67c3efb437ed7e07fc0019d7ffad36bc36f1cf8fef5a73ce529d4b0f76d30eeaa0b7bdbdb2a367d3e87bedbad8cddae60736e43076e4bffd9dff63d7d6a60ccbe8fbef861ed919f8272bc92df7e303dfefd07f3dd46f59b0061176b50a78ec205de031ffff8c7bf0363f4eede0c0d6d0cb05688ff9a281a616ca28684f2184a0d0b38994ca2c6d00823e1ff00483678ec12dc85c1ba0ea7e8b3c32e3a1edc7af3fe9401c376193f66c42edff4c9e56868101504a6c4356f75be8baf60adb5282e2609c80555d1d80e919b112d94940e32d70395dd22849d4d87441e24688ba53649ceae1cb50b4e9220aed0c848d4f6a86c4bfaadd89400c2c4752189505ce370d980d462616325229596b1b7e62663a400ae03d8cac10c917cabbb90dee2e5f433f865ff0eea0fd2ff7f8473fce9e07e910bc7cb1995dd43e77add6b643d8a7e4038038acc23677eb4e73d3a9fd2923ba352db7a06af03ea3b0bc86f4679e5bfe1f4f02d696573dd7b41175b03e9a7a4bbc82825cde798cd258cc98f3064df45e7c258fd7714e858e2d232c7cdd0744a2571dc2c452175ecf376214129687346349d06e2fe90b09ce45d7c1df7aa2774a9a6e5c3bfea538ea892fc32099a8e0057118c72c3d09052da649730cefa6a721bb47f8fea0fdc55077ec679f5bed01a5b7e14cebf1ec0793bceaf3ffe1becd5d79774a51b6a6ed4a9eb2c85b1f33aa91889047403bb11fd4c9e32ed90eb803a252a51746fa667654e408c0f852a36a1f0cc4b50b0d49ef292234591883c68c58b7488c334f5849fa5c1698e781c73eb295d0f1b1074ac7d36a4f3ab38b7122013234ba44176a5af9b567ba61a1dc7da3e88e86056a5945bb9f0b2f2e2e706bbd5dfb9e5992e6428b96c57f59ff6c01b1283bc9cb3cecf928c7aac8ff2aadf7a27b76c6f2e5e3e39313979d96597313fefc7063fbe9e1be1cd307ffc3530fae338ed5b6fc359ff9e1b6eb820e66377a4ed87b133cdaefab7a267efced8db19ea237a90f15b4d4e336cdf8e43ff61e90cac67daace1ecfad1cebabdb137a8df47e102ee019cf4af6652dce22c88d92acb157b9a8941db12a75c1b2202064a7b02ccb2faca8f34daa0218fb3205c1a11e451dc4f037bd92c0f8faeb3d18a83ee651428bf0427fd97e0f664abeabc1448131ac114c1322ff9c8df22af500236e56e8a7f0158166d852ab498579c5c79e6eb5bf04354146474c664242783e658e39e243c301e94a50f00459694c18f385fd701b54ae52ad12c8b647661aad338fba71cbda62a21e77f035a4f6ac98258f58597477e089606d507b15cdc26933bd81fbd05ce2fe49cf973a793c58d3b3bcbf7c549c7351dbc53315b60ecb6773fdd49dff5993b8d31c366c1f0d9a5dbe5d1f9569ccd1ef0dd0177377c756ab2da38c56ef72977c4577eda4ef8988e9d93c5999f2d607cf84a78b66579a5b788b7b6a681fb75f1d674bae7431f2efaa7d7d1eb34a66f4ef1635ba756abb5c8e28178aa3f395bacfe11e37e1d3477d9c9512515817eae21f7da02836f51f40a50166189bc51b48064df85b44c90d8d94d0f7350550ef9f057df0885085ceab32855a1718073b1585a43e888c9fa501eedea2800d515a995311a5bbce43b9d3c03dce750e8ee943b5619b0beab0e2ca1ff72a999b32db48579743d1f7a001fe66d281c0f493bde8ebf3adb976d744175e06632bbe5664411477b035af4af9454c4389d2a8bbbf2d9b401294756d4a728a43a5db431595c028eda954ec766aaae7ebc8093fd06d2d2885a978aa40f2ad7485e99aa47d9f8d78e867b1880073cf2658bdb2aa0a4cce2ba58473196323d63b935812a2f9a54828897462847d1042dfce46bb1df505599df58a58f8923177c32e7bd698f00f215974f0bb6cbeae43ffe16400837f3d7ac6fafff73e6de5ffdec673fcbfc7c627691dd41fe94bbeb5fd8dd5def4e7a777c7bdceb399fe27ea3bf5f266cceadf4c7471c5a9fb7c94d5543a2cb36b14dbf4a5fb08152817c76d4b54ddd3ef51dfa8e73b6ba7af9618f7735edb0b7f410b6ef939ffce463b7b74eff2146f92a4d8b362dc6c319a2956a86cca436a55efb87e1888189598a2360d7a8080531577b13d90d0fdc8bfc26af98cee76de7de9dcbdab524917314be360f67e1e7417c86c886187fe4c9438f5a33efae234bec69eaec4e4716a36187b964d268e64d27f282303b1fb44b477b9059d1dce58075abb5d5a1d9140826ff3a3dc110df24bc0b5fb008b49bbe893c20482d462bd2af0b7e68282f7e4557fcc332f8704c28c70c8af4813d6ba81ba55a1c4ca79e1370ff237ae50fe0fb6ff9ecdb35c737aeff38e9ed2d8a93d5daaade60d83792c37f7c818840b392d07df7de6ab535de2d2ffc7d2c8ab08ac6e9824c6ab77b9b5fb96a8021d2991e32499cda93ddde9e7fc93b4618ff389e7b1891a19f62e4858fd31d0f59b82fd962b766b6867e5ec20ef9abe8ed673a86518b368e196d95421dcd60caaf74951126dd2f2ce86d2ceb1b27d129cc4d63e1446fe0137ac71662a79c2ca26389c3357ca3d3c208f986a5304317dd232f4fbd31ffa46cd3c44a028b7cd3a9c77c9e339d6ed3e11b9e553ff0974b3d133adeb53ada514fd79f771776d3ff8fd572719dfaa8eee4d58ba509b1836dccb126be59ca9703947589791f684219fb38e168970e707f28b95b605dd4b8a42a56d4126d53a7513e79e54c3cfa1c071e94ec30470e70285723cbd12e7261b1685ccbe9af398090c12b2269a3d55529e9264563d2cb3b8e3a6f9ada52e4449502983200cc3133a022285176b929cc4d8c6df166dbaf188c0bcdab0d02de685c472cb60dadfdbbeb65d5df1f4c359720ae44e04baa6429f2db806cc4cc232976e00112ff13f3effda2d4ee7aed24673eb6ddf5af997ccd9e8729c53d1f43bfd11fc7caa993bd3e9f3f9f4e79b3b68f8e491fdb5b7e04d1cbafc31efb869c3d6bcb7877fd201bd5eb82cd45159aa65d546d3e348dbdfbe3ff2faf659b7da70d723a18321192d28899af924c8e182b8c79cee28eca9c36e08a59a9e203a2561a288bc06cf991d964fec2b8e4ba869e7531b4b4396cd2f518c8e7faaa478d5f77f4cb99802795448e1835f2595774a571562d83877224583599f0a0b01cde72ce830c526c005431d7319270a87f4bf3af01edaf462c502aa5cc9ee18f58c432b436572902ea175123870b853c5ac562916c1566dd9063a154bf89214df6f6495167d2612826bffa39fd7d80b7d179ff61b198ee315a521f10f6fad3c96d9ff1a0280e2abc765191b365183be18c5b51a58671bde3f450bdbbe2fd288a2efa99ceb82ab11d079c3b8e4728f8a213ce7a2c8eed71e2696b9cf71e77617060d7a638ec6ccbbf94a1b82e6a18e786f150171c5f74b786b9f423a38902885b67c349a88ffc95e3e2884b5e639f3176de10e22038fe09c0829a5b47d2ea72d3c1e206163c4593841bdaae6ed155603e90a7f3915910bc361f64ec8d42e9ee6b27b39dd7e0a85b2b3ff45abf5c7aae57367647fd625d04eda8f32df01ac69f463d6e2aa5695aa542ed09280520b5888b57023a62421d12acde9864c7b8f49d8ccea805aa29b1c9a2c37a63c7a40fa80a44e24fdd138f4bfd47ff45815b744f07de4afc21319d626d2b90a2d1614547e313831767b695c5a186461f5a7b9f1dfe9a70cdb9863f65fad779f52373a7660ff83282afeda146e407d03bc0cafdb4fe50beeac26a73366490a48eb2c8abd1429336504bddc0d842ca73d1b4d44696f5408d4cd53f69553ab9214b90bac14be7919fae4e2fe793576d9fdc7e35587b9c75f3bbcee91fe3acd711980b615e76198d6d876f8039fe98e3bce060f5b4618cec0c15a00fc86afa7719aff78a3f76d8bbb37e251b0af7f3e348fdd58d17b39db2d78ec205d8031fbbe7633f8dedbb69b005b421e7b43114da8a66556b4a0088d340acf1d48ec5be81568eac7980f52f946419a7ec544ca6a77152ae61d7e62f28f44cd93a7edbe0059a982d672fc6f05d63bdd2f81719da0e457788b5781a48f6808831ded14070ad53c1f22fb076b7fdea36ee48caca2086bb243622061088f9ba04943e9039b0d686f04f3f285debb8defec2b7ae2692829470b6377d2a2475fa0ef57460b5357711962594ec0a617bc225cce9612a471677ce7f9f9dd37fc5c633e7f4d8f0e5d078df21f758ca019ef2d0d755c7de6c76c9a58b735e18d4bd17a9c0b99613bfc5c33b931d4f83ef3d094e1375ca5bf0084a4f1b73d4a9ed868fdcf113bc2dee38f9fae7153f498ec986f4e6e66693ebf3fc4ac6001e277781a314f57a877196500efbe40b8fa1bc39efc7ca79a71f0e76da97cbe7311aafa07cb3548871536f19b338e0aa0ee3afae1a1c49ff3294a556d12147db671da26aa475b4751aeacd1c9449cf7fa95bb40118fa04a097a5fe3615bc19880311d48623738e710e3a4b99cf98284be6bcf52a3b3180139c017a160ff87e8a55dfb17bd06f8191fc283cfa3d905f1d9df0834611852bffa5864d0705441b8dd011ad16b1a9c225ad7204263d8ead696c96582a8edfdcd4113bd292a18ffe01ce5a202c3be7c492847579d914646694fec1535398401d41e45af5585752e1ebbe72f43534b68532f937b9521586b51c3b057055c2c90d5b708943551e3d79ca113473490160a6b31c3eadfd5d64e774be49a05476d92d271e76cb05429b3a48db974a1e3a8b526e2c941a4ac8a209a64804652801cc80a9339f069aeddde301b577cea76b3fc4bbc74f3e1ef03267d747bbeb8f790c835ae17c7750bb93aeb4dd5177579dcd81bfc710be36376c7441748eceb16164e9fb19bf863df946c6ef53d27e29cebaf81753b0bf8ec205d603bc2ffd390cdc4fd519561d5e0d96862c1642338251d244603430d215031587325e9b189c66d2daa4c1a850285d0cb113cb7e916c32f9cdb1932e18976df8cc97f36791b946c3aa55b50e8da8460f079e988440820b40994032c2cd6b448945b12c4a695e38f9c1f091009c4f78eadc845e2cd25a65786a18eb5809f84128feb64f11ad200e58314e3da107c1381791531917f866d724f88063fd5355e17ba5de8c010d91ac4959a93c40b3ba0d27ec7a7cb4bf05835796936e316e39def95a4563275d87b67fc267ef8543e63b537cf3fae8a4b3680d67d4c7b81d6736db3e85137e6a3addda99eaa4b7673675ccfb676d6d87a3e2756e1c7e2775ccc79fe2ab93ae3bde024efa62b1b9bdc009d711efb1e9f639499c0f14de87f0b9746772e9eee752d2fceff03f7c48266dccfb7787b07d46d828e7ff3138e9faea84f9a9535b3cd4b485f13f49fb3d07cf91f73ae36e3973e7dd0cd40fa0a7274a0d6ad4c0c91aac3e462919db947051b3d4909431e41ec9628905e2f053a63e421fb0c0c04be7c40c287ce44142a01c8725193d261ddd05b968bcd64dabf4aaa5ce94e9d082a5846125cfe9eaf8da6cf682da50aff3ea41e5d2cfab33d6390b2a7cff59d0bed0769aa3f891ed018ebb3c87c17cad7aa413a92ee50815031e9d69a35da3ae6ceed61269d3226a2987b648987bcfe2caaf94447de3a31d6c413a20a557a96f948f4d2d1d0e7aea295e6a5dea4ecd72a00a2ea6ea414231ac9f39a28e3b4720c831c8ec74d8c646044d44ca0440886422b9b5400baff449a5adc59b5a9b9e8d18f9a451c12e9baedd166e885c96d5fad00015d937ca46e4144a9b229fe7f8a5b55314292d0b07bfdd72dd8d98b6519c8627af486e5b41b02c7fa2586245cbc977b201f47eece257295e9d5d671de09c76ceaeb3232ddcd0e7a40e7b4f57c9f9711dcb35b62798e937d3a17797ee555f2871f2e9efe593e8078eca56a02fd6fc98d346f90efa4fd01f4f7ce2d6000a40f5bfacf3ededef71233df4916a78142ea01eb8efbefb9eba582e3ec47cbfdcc18b21d19a630c724e7118d1326239dfada1c062c568c4c0683c627b629ce0e57fbb38914836a342ee2ede4bfd82255e9e3be9bdab98483847bc5563397f0a75ff064ef5716994230b0a4c7645a91d9d00400a1ad7c8947a5359f92edc7d000040004944415458870729e5d3a8b9e9ae81ceee7b30b890cf2e663104cb72fba276198b18e3998ac03551995637d92c1e94614c4513679038bca8a69fa7779169eb43dec34e659a7b696a216a1d661de964e3547907a6febd3bcbc9ad70d877e61c88feaa6e2baf62614f5d9a7238b9b6032a22ed09f8e3525468e7cc0f3af08d310c5e8f3b89fbe7fd5cb9cef92e3c8edc41acc6283a7b25e31e685ab117b22f87b34f5dffdf004d6a373bc0cf9540ffb23b7e361c16b9617163b11b74557cfd7704d8981c735c36875d76daf3d780bc8145f871ea415324548c91cd78aa5b8cb39382d10e8881cad8bb33de76fd2c905e3d2ce78084ba0b6e28c5952e58b20a76e5a569f072b08a16ac0e1d52e11986ea37fca28b00a2abca1c79efc3d97fb667d5b768f2c6e42447603633b6071d81397ab7ba3dfde887fca0d16cf60124c19696be4479008cdf50d575408752fbafb3181ba77e45adb834fb1b7dd576d93c7583842886b2ab248059ee27e4da57fedc28099fa6d465da849556071cde55afee6978e3202b539d296f7c29f0cd3261192199713440d3eadd08b6a8e64e3641e06f657243963a3602a6c20b35a672d73b6bd4812e584a5b5b140cfa542811d9f016c77e80923a0a85be4e7d5526b235a53a2ea6214fdeda526b8916cc8813e4709649e0d8fed42171709025fe7fb80123cfdf6796f3d50fcd27f3f772ec059bfd69a6f265c3ef1eec1e85a91f0192b74e6adf65377fbe842e5777a275aed7d7e72f61e46f8e62a6134a5fdb80446f19fa17edecacdea56dea6de9bbebd8fcc59557ee649dbafffe3a06d3dbdeebeb3487392ecd3fcc2d3c446df3974751f00f625cbece093fd8216c58de9dabd573326855622a6203e33b9645145e664713a9718a81875176391a8d660a1313fe38222f5c4c17b78f5db172d2e535b964b158bd69365d3e396cbd61402e5f65a7dd7373842fec1185bc7510e7c622865a090b06451975e96114f31dcd842ee460a62c75169ded04a6ac86ba0232e131012131841acb92c5588ac22616997f12fc53a22127d9b9d6a2479e44f593e5204813a69649e052618f919c4cee43e6f7f235dfefced617777b2ce5ec61c7e11a30fa839e420620c4bbceb6508fb4780c70e45393dc3eab63ceeef2f6f6420795970c0c8e390eea88c1c1121ee094efde24ec2329477c1f7094e5cce250f708fc2527cfe6acef878f9df6759cf6763866a88feed8586deab09f664867ff25e3f53afaf6718e6d1c7312e5fc0c246ddcc9ab940e3b83d71daaa6d28e67a3531b08e8499f63ba5e64028e0aa969e4dd612ab0f9145b3be95c238f8c553d18c805b61120f46002dbcb8339743df4bf339f2f18b3731f81b9909c81ea9dc377fda99ffda9a7ccb7667fc8383fc9516fffd197dc0ca2185d572c5351f2ed6719d4e8a236cc6fff74e063ebca784637d263da64f5844cbd0dc69caa9c6d070a4b87fa37b3a11143a4c4baa926c143fd426d19f9a0c8d83acd27029aca80132b6fb9f0cd59932876540d3650da68639a298f332b9b96b622500a1f20c904615560c5d2a5b10503dbe2dc98044df9984f79d0d3fe12573afb98b2204b8a5d679ed5ad00f9f0a84bd5dceab23e7b461efe272f1f128c53a27e2340791805a7903d82c40df73f9eaee6afdcd838c17c7d3c737df7958517d2fcec4e3aadccb775ec905fcaeb45ff943ebc3c6f52a333a205693ad0ea82cfd0efdf4c9bef96ae3bec71d6391774f96cc937a25766fde06bd5ac41179bb38eba1c850ba507eeb9e7e3bf8d967f5fc95b4660c924d7b1d046e47c6b330e5a0ba78446b876049ba1d14a6065634cb8941311ab11eca2a3067842f19ec56cf9cab1936edddd51c749f959b09e0d610b24644f4e43181e44c6652aa957a756e317f9fc62d6bc06523cfe49bb63919d41e2c293bd854494e79c63658009b4c83fab2f5ed51f95aff292a763979f63bf41a18c84f485b19cc217d9482a65ac6d61d54d83fdecc2280247b291f3bddc96fc6b5e32c2b9f372b3bd0ecfdc8676f782112fa4c125af873bfbfbe8779df3d0345c7debeea46f4ca6dbdb315add49efdc3174e5149f3a35d91aed9a9fcb397fb04ef9410ef98371c239da1237b3cbf8e5c69ffffce7e32beca7dfefa88ff3dd69dfbfcbde786cb098cc573b3b4fe7f6ee1606f47119522eea6c39eda803f93c0f17a2940c7a33a88b09425126a102069a372e99e960e7a0ea43bee628790bd52b53fae15e3a031436ef94864859329f64903c6841051e2696af3e3a9dccbf9baf9f4f6de4f1ae1567d5d787b7c0789a08aa3804a487b7c0f4afb1fb4228ce5178787b80e32e7a66ff0eaf855f72c681640ca3298c73f6051cef66a4b49d71731df73ef6e243e0d88ba6f3a3dfa833ed4e35a914e4014f3ce038906a88be25383ab0ee51586b6e06acbde961769a3b7fe4b082f89cc645422c9d95355820962a485d6bc71aad85af6cb2db8ec07544c6eac2adb0ad461e112397e8b7b83527b0cb6086848b32ebe0878a46393f44501aa1222a499cc2c8981c30e50999ab1078d5cf91c5be118d102e14672db23b0d854e1f526aa7f7bc45304d8dd267c74a025b64a0a61812e5341790c5fef36ba6ab1fbce4924b1e103c76d6cdf76fbe4cf7f9793eed2a8f65e90e7b1e2a3d7efc977865f375ca1dfdb19fe90ddb1ebdce0ddce41dbc7ef8c5c1e1d29df54b782bcca7e3ac5f9ee399f7df7fff190f97769ac31c97ee1ce6161e92b6e1a4ff38aa7db33b0d391b8b726bd033db635128d0f03105747ecb1a6a34ec002606847136621024346f1941183c649312f850f60077c0df8b935eb7b01e2ac02dec4e3a66e46a2afa07eee4e78c3cc51e4fd144d50371da562ba03c6b8bf2524a669573ebe5ac2b73ea6cc68d6c4241b99ae733889ab60282a730fb43616d77f8506bfd4846a3255f9820a6aea182620cadbb93f65c787479ccd1c161df382b48419aecbc2b1609dec162fa01fa254e7339e9757025bbe92447ce7773b8a97a08bba03c0cba9bdd4d0db8b404e77cbf636e7177cec70f7fea98f7073b7bdc593d18c7fc20a7fc58db19bfa7336af1d811e7b5a176fa2312aeb8e28aac75fb1df8b1a33e4eebb4ef77d88f1de3ee87171af2e3485f8b3ebd85853fbfa6ab0310e7234aa8fa940e9552942e44eb9c64d195d2c0aed34e28e780379e7dfe053ffaaace55a87957b479e05887447e14587574307ad9f511ba4e2cef94012be35045cef7c9e48594fdc17cc1aefa6ab5986cf223f4fb8ec0e80ce8a82b497704ce472740f90e63c049df404b6e659c9ea9b3e8b03ae6ea89e31e0091e94163d4c3916e88e666c2743a6f1c84a83fd86220c5476736ae91852db84e0c1a0aac70e3e8aaa303bacea5f28453ad15e6d43741596410dab474da72e5a33cce3569fe8bbf7069f31750bf84ae668c7cf94053cf5355da0a981a29cbbc902f59b012a78ac1112f782acee650c3553610eba84c49923e0e5f6bb0ae545c6d814d6e70d2e7f214c79ac0ec1d246debafac4bb148d6035efaa2a824ebbbfa65576443ff43db8fe150d74719c7bfc3bcfc8b3a0ac3d88ee6e8f9bebbde9df537bce10dc77c5b8bc75f36e7f3674cd6e6efd74ed57871ed3a621fd997fc917a1eeb12af28ae3038eb979cde99cd2e5fdccf0f455dc9cefa7e67bdd7d9e90e635cfd76185b7688da74efbdf77e134afecf69d2a6daac4ae74f7dd78044d70b56f7a9986d0d5d73662d0e4afac414a14530230dadb19c9d38c9ac6e982d97ef136ad00bd557cf59f5e9f4eb38bffd4fad223b2932693cba41062028c1a85593443f2ea0c51c9c1f704a0626737c6b79fa4fc638069876c5c96ff0146930ed876affd086b43fd4d41d86d6202b5b495d10e10ca5bd31b8e57cd7d7bec0e3e880ddf0d200a961c5ff6db4ff8d3c90783bec46a1fbd6ed84395916ad739e7ea17c875dcf7e26bd3318785a6e061cded0c2d39f9bbca385ff9dbe6b4ed9d69c37b3b03fcace703bcee2c39e154eb4e4f1e3c78bf7e73e37993c7632216a37168577a0537ecce32a5fd15911df035ded8c7f29ce7877fc468cbeec64dfa9391b83831cf7eea8f758dab339ec3b3b6bdf864ebc36cb86ca32e88c4a567a1707c422f3e85d54844cdf7d8f9249d927800090929767e8fa02adaec98cd01c0fd45214d593c0453e4d0ff58d845554fa6b9128d1591caa4ca43059de3699ce7f709bb3ea1b6ac61771d6bb13e0785d0c8b9f3df968871b6eb8fe57d8457d51f9c4b14c19eb387c1a428d62eca4e01a683742bc29331fd5887da3252997c46f25a5231d4d69fa42b9a43d84be9507a68db55ca6b1b7560d1341e40d290f1eba57a0d4955d79f120b5b6fec20285089a75536259b583120ac2dffb054adc60ca4d6d93535182238dbcd34955697ce15c8a6bdde4842198c5a7884219587dd5309223fcc34401ccf1a78c91b2d55deda9be6a68851ad9b37484aaca7affc0a2fa8c0e714dce2e7a6e18204ec38840ceb11bf9199cbbe2aef8f5efd9ecf917bab3de6db5bbea6ce4dc41e39e6c1beb9b49c6c83e4f87d9ebe9fd0fb169f0b7480e4167dd5df5c9e31fef266136148cbbb3ded796c36eafa29e43af1c25cebb1ee05cfa53f85aefc3ccf7cb99f1a5d0287777d0352a86322e551e9587401391899059d0f08cb4a65a4003d63625f24c0abad5ec365e90f1e3832b8787ee238c4c103673a757f0231c6f82fef2f091059f1832eb74e2b1beec9a3cd306e5216d7dc1b13620ca117952da92c2298d57423a06da72512d83c6354c7ef26a707967d7c90aa565a1133d98d00495bc38a2c82b8b0db04eeb4251ef52975e0989c1c309e31595d30f0078237477caf5ec61db37b2c4316e2efb1e547cefc149062b78cdc30e1e7d99f2dd5dfa7a81e2ce4e1d69e9bbe72263ccc7a4ad9eeea837e71ce8e7e69fdb193fcebadf313f76ec581d97691c8c3ef7b9cfc585bcf24a76c8b925b9fde900f7dd9a88d78da5e9834237a20f557cb63af6c375dac73beddd51eff14147623cbbcef6cd8fa02e2f8d2ec9142589ca45f76a3d1df4d0623eea961dc871cad22995166fcbb20412eabd7a974ed5d1a2545d73b10f3fca84eefe3059388742c76d57574b6f03a334d303c64b36cdf566dacc9229ef665c3d1f7dfa93dd5df5cdd1eb1a4f6c9f3ebde12fba1eedaad38f8f74b8f1faebaf65585faf72c5d23465311d981142e9c206239eaada63ae85a693590f0441105d35a16e01ca439c5094c6552c4e71ee0e1374e0876f799fd1691dab9cf7863a8ea518b22eced4a56c58cd22246f79102a221dff53dd477e9b50f5208d7aaf7cf2024fd2f00e2fe1243cba23736f0e6cbaf444fd9a1b84d015ae73c2d0c9ca49b65d3ac0c8a068ad2e65ade32c72b445c5bc7ac1def28fc06590ada5dd4dcf7cce9850b78cd39a8a5d0f8bce3c41d6b69f64ad594991b65fa9a73a9066924fd1f26edaf03d8bc5ec4fc73bebb21a1f5533dfed6fb7afc21eeda02c5d0677d53736375e412b7fb47aa78d43f52e6894a81fa4188f17ed6cecbccb57fdf61067fd924bda9af8c045795e1df5390ae76b0ffcf99ffff9315e52f44e16e8cbcb007a078e2502a0f16e16b00c86864363a4b6f371f2e76123b2718c352871389c14e27831ae0992878be4bb9a9d6637e41774d2f36e6f7148eba4cf66cb7516f55741c41b67a09765a697b1d5f2671da61b5f254d89116586942b3f09cb234a2bf349f9d088ef22e16d77ca2a8e31c592d9be845e463edf2214b4f82aa081be51aa2e4251eed2679113d52a125bd6245fba7f3d7d075f4dfe1d8e23dc782e271dffdb5329decc6c1ff4d66fe0ed758a3efbb9c107271d73d63d6dcb314abe42115fb18ed2689475d057ab8d2d1d743f8c417ec8c6d8e68d824e3f9fe3dbf3f9634f820bbfcfe5b3fe85f585ce79ffe8988f3f3ae57e381fb9d33f575e79e5d24ff8e3a47fcfd3bf67cb4561fce97577c37c50dc17928efb978dcf56c77eb9dcf5f7e1233f1ecda14f73ceb1c7f46d020b616e96948b13305b78aeaf61f4df33cc113587ffe8b6c9e89cca8f1e3a1dfd23e1d12fe7a09fb8e33a175d4f01a955ea60392ed5ad160b8f83dd78a9afd1efe4e1491c3ed4abc350cf68c83b84e87d8014a8e7e083a7e3af983ca8761dcd5e4371d6783fe59c07b4e6b43f8be86231e139db2ddee678f0eb1aede7877aec94f8284c26bee185e78bfe37869a505a53297219d86e17b5b08ea77ac2c5b16de36eb65053421640aea52739b70d6ed60a4b42c7057d919f9ccb75acbc1aa99ea94e06f14d87af09caca3a73c40643694ebdb55c09224523b6aa5a0b284dbda28653f8d82648a87fa8247c9275dea47ee718e9214f567980115943665af107b71a909d5acbf39e78bcfb9a2f480e1f71e59d86d9d6d6ffd50ee0d531c1890ca4b27e1651d1ca870ad237de3d9498748fadb14064db501cccd71f32e46ec3528802871c66e977f94436e8a6f3afa4e4edcccdaf758ef6796a311b0c7b5ee1e83ced73b5db46f11eadd065e9b6233f58b49afc96dda185b4cfd5f1de4ba583e6127e72766a7649529cd73cce67b1b958c786e78d5ecbe5e573d6bdbcb56bff2b1b7b7d8dcfa18a86de3954ad3a248db9e7be7bdec439c06bfa1b1e9cc60e585d3592fefb359f475dd47ecd684c45b032e985986806a29c8c00a02d4e3138d28103ff5fc640bc35de7973d673e465e96efae4e5707a7e555ce6df5767a59e122cb6d2baa6380d7583401d94e96cc8bcd7e99b61b07704a7ae013e9530052db85cc6af656c36d40259d5a6790376031c165c92b78b6cb7f9d4c18582ec5035fe55580b883c4d057739394197be9dafe9de8a61c8ab15734bdff98c628c2c8e5ef7f576cfa79bcab9f3865b78fad6fa49c4fc03e3d7edfbf9f6429ce39cbb87ae532ee40087bc10ebda2b161f111595a76f0818b261977cff8ef9ee6e7973c443b1f7f2f4a73f5d6187f0b6db71dc70d66f676178fae8784437ccfbe381f0614cecaff3a0fcb8faf12e7b37f83d1e1f87d9d8585dba58ccdf854e5cd5b5a7e60edc5410268d7fb9c90b881cf0dc50a3db2b14273a3fa047ab513f662ce571aee5208dfa8cb27137e6f29d39543b6c94aba71c63715e8f1f48050dea2c7ba1908f1322b3521a67488ebff04dd072f23759ddfc71115ed788f66d2cdaeb1a4f4ce62726dba7792d1c7d70b4ab4edf3c128173e9bc217af97f33644fce48e1ec62c163d3d4299d46c68a58bb5e16dde15531d41f9dba8c75b3e91695cb0a0fca4a5be423ad51aeaa6c39954093065c45e4435794bb85eaa648c64856063b30c1e560a1b1a4bd44f74d5aaffcd475ea442db1f5ae4fa5cf2547888a57a85bbbc0b3058d0d75802741782602d25d3bcbd2a4c499036910b0c85df5f5b7bad8b7a10d9932a616684d909701c9c212661eb86896175a8450c2a227015d7f330f8e3554c5671847e4ef5c225655c28d049805a0eed626ea085f5834dc07f876fbeff2abd5ff9e9ab6c7bbebec32c4bef7674b283f6f76d7bb1dde7356fdd8fa871997abd469ed9c7d6a485f1a9be542bfdcbcb5b6f34fdc55d751e7cae7043f88c431183692e8e3b4dbf8623902533d655f1c85f3aa07eebdf7ee17a0b46fce6475d2229d062c53be193c05b65c63182315c56fc829eba6a92f034e10ac3da1bf39a514a03b01b38f4ea63bd7d66974b1744dd7741475069fc5c3a53f57e76695a50c716c68ea47322d0c8072ca4937395d4f7c15568c245e43d1c2510347a444c600a02d83a8e4e6d3ea381c66c502661d29811662ab15e2a2d26e1f602d9ea81d3718551ea3d81646d0e4a5e11003d7e73e1ed67ddb74befe6e36ac4ff66f16d6fa4f878267b06768efe02407984b81fc3122b307e1f4b7b58c1d747cec189f7ebc853edfe324eff2dfada41cf3dd92b1632ef420e77cd821df254b6abf532ef0f6db6fdf107e3b0efad3470e7a77d4f7b178d4b37d71d81f77c184f77477d8bb93dee3b1b3ce42f057c1ff178c21df20a11dd157e70a1aa37f12dd425f5520f508453799609e44167f6249a281eaaa692e6ab3413c3fb9aac322344841ad9b5427c8142e58e4123e38e9409c14e0c7a94b3c7ded7467f15a1e96dde161ad9d0dcfaae3982ff34c03cefa1c677d7404e6e8acbabdfef0851bfee1f5ff2783f94c07299b11dab3280575668cd1b10c60c9a0031c874e9dc96e6ee999a8835a681bc1d306ab59d1494095d681c7e5958f15343b596f4a21df1c46eda802183576a879955b62d0c6aa7e2587b6daba90a20c6851676e2887dc4a06eb704b29d63eceb870ab06afcbddda6741a4883c8d87f5c8af18e203c34d35b7a1c04a0e13f2edfd906cdb102a01eba14d691a230872ffa13086d46399c98e2753db23df821537e08d8ffd62326c685f5f67cb507434f1c33ab13426f28205eb6d7d615d7504c69e66046693cf80774d7bd0f24067bdcf59b9f79de56e07853d1aa1d76fecee373f16fd630cdc4d51ce74051775477dc069886e4677dc249bffd7f3d3f3fbf38bd72347dd767467bd3becfb5fd9f868b4f5e1aeb3abcdc35dcf11ff2fa107f8cafe2bb85bfe7fd0dfcb63159b7d2b23c0d5440c0aa683b95cc6193547e96388206aee70297ff029c7e1b53cc63230921a07d9410bab6be1f551b2157035f54fe7abd5e399506fc6483d2e7c21707e75a3ebc4eb7224d1c845c9e4eb5788521bb2f785a71ae3d5bf0ad928d2ea11327dadcc6cb66648d73f71d1889ab3be2251d6a63cc5d467a1f4fe1b2b81c656dac8e1ae8f8bcff22ef634de3a59aebd979b0a1ecdac9b14654888db9d9f259aace3a027dbcb5aece6b86e7a76d2db19f5310ab2e4a8c518d677cdfbeec8591cf4e186e05ccef9418eb9751de49cef77cc779df2ee9cf7b89cf4b1cc17527abc58b88099eff25f71c51738c3fe44278437a3c357aa63679dfefeaf28fd67a8cc6674b9e950393da55aea74b94e2e385d8fd5b5d2b1d26771c10a58656e78d1c5e260b242e59d9ba5ff605368b97aaedee687cc903c7a2f5370557ff107a72fb5a4ae3b663b8bef95378ac4e91ebe4ee68d40fde1e3f9fce8acba7df248841b6ef887af66807ed4bad400872e262a068e3c4638163ac3e9800f431b7c775e871f7603a7e84dc0d0a16e41bb575a543a193b189ea28a68c5942556b7da4e309038a3c191a518cd995507a594d494370da15766a4a6a0d410be1427ed450e447144a1899e17388dad3a74e4230df84044522662e1ed3f71f5508408aec0bca35bba22abbacc03a8fe050b59c2376ba054d4279b7ea352355b001e6532eb21b44d8ec86e5f94bc3afa0c531a3c9d0b4d8b5957ba8cf26be5d42dd7b42bc5f29017fd972ea4bda95b31940142f8109d00e7fb59276e83fc8272d691b787ff74b9b3f32736a6ec999d926e4b6c53d3eff6db6a72cb62b1fce963c7c61b56c7d9553f9d5d750974d4c7bbeac2ba8def372bc20e43a89e3a0c2d39446db8f7de7b7e0f8d7d6ea6b016c02df39c3f2552995df001fb71320f3b1aa46330da36730cbe482d143e190d509cf61afed04f57efe47cfa2f74dc8a7770603c9b3e7b154ec633c596471ce0c1885823254cae542552b6dd9b017252f2e7d5e01c3459469c84a45e12c02cb401c97cb92b410d5dc789510edf56070555bd44c092070262f1312e5c01ac0313f616bd31b965be9a7f801f763ac0ff0624941b96c5d4172dee4cd604791ea861eb7f977b4e55f8ea83570d0a8627583b9c67f1b4bbe1413ae7a286d5d8393fd7aef9b98eb38c1df3b33be5e59c5bf1610afb9d75dbb6d761af874efbaebab1ceba78beca717d36fbdb68d42fa9433942e662ab47d516fec0a3d0c25c889d13e8a2baa67eab8e5c72931b5d0c23809439570d94d7110571a5d6a90f42e1819c7aa2efa2c32d78455e6079ecea782f4f3c5dfe4d7efdef2e75c95d75e686cf3a2c3637eb87aff61f81e93b748775e16bbdf688461c797901c6f1cdc326851e5e069531431272d85b5395ae04d64f1c0ad5a7be11d2f1c511ac33ab26c9a3708944a7d0b8ef5a2663be318883285ec8b816b3d465b2d686b299b5232ea276b69c7165ab875591017c57a61c3bd1218f34d5b6c85482042a4765b25ee96a3d92a732f01f7ae084f011478192f7820464a537dde5cc0d499b3b39a31e84115e58880f5d18d016e91b9f30ac6a0aa6f79cf646e28627883c787583100f3b32bb544776f94bd7ea0b7ba8edb94ab794370df4618e15117b84a682b4e0864f58019e9d5ecd573fb0d85afc3e993dceba346ef6f4796bbe3babddfe097b24c3b85ed3d955bf64e30374dcd7a71b685bc6c846da9f00d35f349b7f56cbd9b7f3637d7f36199cf59c83199cf5bea36e7cd877d5bb563c92e37754d7397ae013f7dc732d5afa5ce767a6b2f33adeb9111027b5daccc7228d81699d8038a618fa9c0794be8a28969bca5f0e44cf853499199362fea6200d971d76edf29697e7c1e89956590b8cf596da0ce7c76360da24b3528ab53fa1513261256d8b8a433ff318414541f6329a6939b8b6c57e904cc388210ffb2e01190b8531d1ab3e7093b08ff24ff9c08464d5411f3e40f215505fcbe75f9fd549c7439fae4fb7a78be9f69a4e3a7fe579a79ef670280f86b60749e359b3a58e0c3b6b8bb553f845fae87cea4150c6684b83cae341278f71bcc50ff58f832c781874de1e069defe89cf7cff80150d3e38740f73cfc09139df3fed139b792b339e9968d9d79f38725f405ab3b9dbd5d1dee43a797f2431a63c3cf1825b0f06d6f2f97bf8386bd4ee573eed50da0635cce77f4cc151a351347fd8b5a3a4d32091b5e1c885e7bc5aa2a04f984aa396a71b0e492b29a6f52c441005eaf7593d8e0dcd0215236e681f3b1fbff9426b99cfeb73ee03c5b2c7203828cf37a624bfae3135eda79d6b0bfdfce8a785470d61ef0e15106e7d7630afbb83ad6008c18be8a33de9aacd209511dfdfc72b4e34acee5000c871d3c6d9f302da47c801b9a132ce38210634ca3238500be7a434684d0351e9207a72e2512cfef018cfd34d1f89693deb1e50e8e46db90a360e2c2370e6958547d607aaf9b0c1de0a649f8abbf92107b631089222408769265ce352995396562559dd5dabaa5f0d98ebe10b94b5dad87affd983c7da7639c0a95a4f547522dedf99ac0ab6d2e48ce31e555646662d1c327eb6c804aa18ce0f9510c68c2c9baa5e382c1497fa609c094c3b2cc5ff3b248001a19579b74e9ebd939780ee03c087ed043a63ac492f5cd88476bfe8eeb55a6f650e9ef9673ce182165f9143e1b4adb95ba77ce64cabb8897ffe014afe1c23eb17695936e8e6f02f37069df5811b6bfcdbded961d8660b71c85f3a4073cf2c236d71d38c0fc9479667c24ab09ae0e337333e19bd1d058c6c2e10c33928371688ebd965053570e3ed4ce770c4516018d982174cb776279f6eda653345d5e8935f94d7649f80118ad4daba3916692c59a68a4dc012c86dd9129b4ba66014148775cb4b7dde109853299b02c953419495ba241af9d96824b1f49e4631200a2996047ca854b3c4a00f60d4b39d9647af534656fc3b17f13bb99f5634ea13df382311f6d8ee39eeba1b3d1ea8ea4d855de51ea974579af8b6f6e49b938787bedecf90e6f5911d21cf3f6faa91685c9984ecc4b2ef9020fcb3cf8b3e6fb1ded7339e5f2bf5843dfe919c7f6c5173b0ab3369dbe0527846f96d44a954b3d83502554df32ef80910cc8abff2913877474132d1fd38a048e3f04e63977f557c72bfc21923c4e8ffcf3ed5a39fe6d36c93472e4a153044a7d9008f706de1747e2e87d643a5f7ebf5ff71cb8ab7ee2f884d7a271e48b6adb83a5fd21b57e4363d951f8d27b809d749eeee697475793fcf268dec6e5983aee8c4f7f48388e27c3e588c5b6aa138ca14e251170f3b1908d567d21c928e7bc796502eb58b549e32a002dd7a04ba252c94bdea4e3705b97655ead3b292ca6c0800a525c1448a0f846f22201cfd434149170290b465db39e0902d68fac908c7cf1f1ab9ac2e8371c2220930e6fe2ac6cce937081d83ea83533e430a228fd95eabd085098d6a7c902b4fdf2f5dcbd6b4435ab127291df4063a18054951aa0a37fe1593dd62ba24f4d863fe834ba1ea294af052e6a8e0bc13e635e073d599cd82648d6eaf40c74e0d93f44a7d1831f666d7917bf8f313971e24476d79db7b273cd391f77d6b5b7c8f60d34fbfd5d5f6b2ed80bea8fe360fb6c64fae934e76ebf99cdb18fd589bd63c3a6d6b98ec01c467bd575c3f13d0a8f720fe000fe3a13f8712a6dbc5327bf46c54f3303a86f2677998f9af0654cbcd62cd6f861c251fc32e41aa14ee7242867bd2604ac4ff353e36f3aa8e9bc99e57a0ce1719f4e2f2bec84b24ef8c56874aef06a5289916420c90921540b5c18524ec5728a61d6a88aca25c6ce4c58979b1183477d83610335ed402e6d9ea4e2bbe0cd7074b2bb24b06c61cad33bbc071dba6ba68bd53f1d9cf4ec2fca6037e880fbd985c443e7e80b8fe4e1a4ef2ff7c151c3daca1df472d2c962344ff1f03afbeaee9ee7ebbbbe7b8e7165d7dc0f75b87b3e38f73ae7fdf3f9cfd72b149563fceac4f199f3be636e3cde35df957d77a77cbf233fc6b998d26733e49ffce46396975e7abf7e843b6d39ff68dab175679d07d85ecc7c7cc035d479a98a969e9a4077bd5935e58223483cd2e67402cc071e275d286582fda40cfd15c3051c40e64a15848713c67a2360785b0709a3004da9f48d77ca485bc1647515ef647e92cf4fa0c3676afdf1131ef3d903efbb5472351cb65daa6ad5c37fa5f7dfc6203fcda1cd2f3963541d356d686987464c05d04a69158dbd6223b58935a40d5ee3ad6ea809ded4c5e991223ccaae960e40d2422c29a4d10cf164834e96fa58870a045036cda696cd946d9347bbac6084e82619b3ca9c943c4509d0846d2144ffc80ed5165da644a3c986936d4220655494f40fb1d8cea961199267a1a44ce4cc2fe1ae25ed2f4894f539589cec89aac775a2552f960b082d71be80a9cc161a480e7d1c4c38d82741a48dca0caffec02ee8b55bec5d84e474addf32d8de92cfbe8e1416878f390aadbdfaa9cbd62676785b15702836c1fa454ee7fd6ddf8682afbebed95eb32a3fe0797d639fbf7dde8e77b8c57ba4c2b85e76d53f445b4ed8b7f679be194c5f568f4426f2f615a3b3891dbc16185beed9e51a9e2feabbeae3363cf189f5bc516f6f8fc738176ada513f0ae7410f7ce2139fb896adacd767835c03d126bfeaec7fd4b85b3a2731e51a8b1818d11d4961bdcc092d8fd05679e1bbf35d13c132d0dec339c3575ac3383041fe7ba85faa01d1de68723571995cc262404ab654058c5ac2a289423d9a9daadb22a90db559601b3a0c7ee12ecc36b0042163ec200ce2fe889bb6654fbe55253f2594671960f92be3505700ab8fd2025e3b39f9706d8b0b1ca5dafe37fc47ce79e1785dcc163b3a38fd478c7a89c75a3cd4d2f33b6b38e73caa87a1ccce06f06107a0e3b8fb61ba3bf4a6798abde34ff8819e21ad733e76cac5358c1deea35df3ea932ff7aa31ef8efbd8b03326715afd7ab57fc5cab8ae2db6b7bf03e57a7d4dafd232b5d7ddecfe20b77a5d81729d77f23a19ea656684e5009db3d15d9145129e50897a2d63d1899979d091a8af5ec92ac72a9161d5217663265beb92ef72f50a5e01c9ab57f7eeaaf36efd93e8655e85c62dc9d1aeba1df410851b6fb8fee7e8fe97ef8e880352cc5b44118e5b73ea86d156313a9eca16fd1280bd2be523956cd062db31981e0194555945626085a797270141658c512f1b2bb6baa89b28766e0e406bd5147df2e28108799c4ee272568b5e7b2d91b264c1302f88fa56f85afe0648cd115828435b55eae6141aba41f65e7a6900438622a5f15f589049784360043c5593d6319655c12dd8452f675732da2a6ee713c6c024737db39194831946e52c0b10a342950851e620478e8c516e545897daa25e4737e96189e49171209f3320d093576adb96b48d697d59edb6a0a4f16a7dd4fbf2f9fafa2d723cc143e11bbcbdc974ff56ec7cd859ef36b6dbd7e56ae75636079f99511eb7d5b6db64db46ac3e02f80c43f18dac975f60e38bb5310e7bd6d5d31b3c58bad37f0869ef83a5dda6db178721ec6adc6168cd05da068fbc60185e9379691b48985691cb6c340556a92d8b53ca648cb11337aa8d5d2953ab7ec77180958aaf41929778d9fb802ef7f4d3d569168937eeef368cc45762e87e301264c27091172b8e1f177e8db9c628f2c51095a4915de1c5c70029d910c08fb1521e7094cd3a8262bb00c4e09176c121d3e845e463fd0dddf608f3eaa742cbd1cedaad9a9e26f1cbabf9f4ef538e93be37e889f9d141f7b3b7343b8f3b9ce965bf9cb7bc8cdee2a2834e1f9cea4efa1a0e7a3ed93d8f93ae21199c748c0cbbe6be59e3f470ee5ce7bc7f74cefbe709a31f1d1a3be967db39ef3be9dd79eff1feb61ce50fee816ed0fb62d2f3fde1a4fd3beb2c8abf8b0ebec539a067110d5497eb6b1f5534f34245556b79cea3749abcfa2d4cd52e0f0164f1d56b63c006e795fc9db77122c807ee053c319d571a6fbf3d936bdd845b4610479948f8b09f5959c0eb596b6b2bef39f704f472c3afd0fd9ae768577d4fd7fca532d7df78fd77d3fd2f778432e6daa58c0a6c1d702361163ac419a3800b0f14e8eb32a0c0418717b0d7bea11116ea8dfe9fbcc5efeb039a22be7596cf48217871a8498aebce7c94444cf2911758d68b30031899fdd6123e8d4efd8c0ce1df80d2373ef1a141ce7144d78394598530b74a91d92a533d85f0934b9358c49255271e62b35d6ee74929b63830a0d439919417798622d2a43cceb905b69fd85df52120449a987279418dccfaeb9eabce4d09f359817bbf44043993704d740db6ce505b8d0db60a79581171408295cf3e815f1d7983ae7586bf432217db95f15426176da2ea2389bd459fdcc46ed1b3b2b3be38bece4e733618b05b3e107f5eecac6b53bb7dad2ee017be6948748136e446c66ea5b9342afd9c71a089f4cde3a6f315bfdd6238e661f6216c6e6daeafadd50f21097443e5b0eeaa477786961f251e951e582d176fc4f8702e1d35cd4464fa6951d1dc2cd82a303b2a314cced438e49802602065c2d602e04c9689910a9f447808d408e4bc7acc0c3893e9fb38de72b7a9716083e746e4d88c8582850efe10c297bcd5225f7e69adcf2aead0a85121c5d6975a5b9b28601767ca57ac6e20c9409b54bce523cc1d1768fcd8fcf0af96f5c52708a2269411d3e0ea0e790dbd754f2677c0e13a2cdd5b791074d8f52ed7bcbbe83ae3673ae89c7ae015e775c485732e8303ef2e38e33172d03dd6e22f87aef97127729f837e220e3abb953b274ed4cefb81cef913f855503e94ed9ce8bf040ab3ee9c575beb41d09e36ee4e798fc76547e92faf07c60bca41ceba5c7716ab9f40bfee427d4b7fc9384f0dce39b5df155447aa1f87c93c46ffbb6395b9209d347cd4db7070d28497fa4cdac23de5c9b5f921aed3ca7ad0f4305286c693fa72432d13f098887f9de32f978c8fbf206f5bfcf0d2779fd7b29233c2b86fce283c02ece9819ff999fff5ab7142dfa4f5aa404a7bd6f4c4e15037ea191acb840824760063caa30119ba2aae71f4988816ae1c43f1a57378e52f13cb49c023592f3008ebac1992343d0d692a93b0f1c29282ac53eb6690baa548bdd84cd56319e92c55c531a24063ac3c566d89732130f3d0d4464a9389928868054993d7e155a12990defadd254f2260df546ebf59062038f26df80adde88ade2b81729d731d6825ea0e903ce455481daf672db391c844c36d6f4218d6380c9d6d79aaa66d22a50d026a3e5be65a658fa7bcc9eb88f91f1889d461270746bea5ab4f842b0bf039af8d9dae7e65b5bdce0fb36573e9bc73d6f7db0d4cce076996ad1cfa2acda359fa037e739140dfe55b8ce5f4fbb19fdd49371e8ec06ce1ac9fc459efdf7a4ad78ffc98ee1b2fa62fe4d055ee426ec3052dfb7df7ddf783e8ebb3bbc3aaa1890a63ec9ca4de511bfc81870a0255e6b61b0058231f53e7c4b54c442ef2028b4ba604b09883c2e1811498bcb1788eae53df8232bb0aa612832ba3564e326269e498449aa2aa0dac12bc5029cbae8b2c94cfbfb25064a0838fb19fbcbf57f9cd8a4cd04897934e5ad9ad2f7091aa0d129872d10a3f04cb59fa09bbe8fcbaea743e7f11b477923e20f0469bc5627bd69df4b6c7c82e84bf7a8653bfbee798cbae83ee6b18eb688ba720d8c9183be8433db5833edfd639f7d39d73e3be732ef2139ef08472ce4f5cb9bc928fb0ee9c1bf733e73ddd1df21e8b7f141e9a1e389b41dfefac7bd889af93bfc07c7d318ac737364e8fa6bfeab33ac907a5f59f74e9b44e953a4c892b350953261a34f3a7d2dd6dcafc756e80e56476a997bf24897502acd31b6080fe596c941b84aa1a14f952c4cdf77cbafaa67ee7899e0ebb6ffceadf06a75f0c799bc478474ee078b1352dec281cdc033c3cbac16300b7e0131fcf309722e020b249e19065801c8f2842c62bdfcd64989a4ef8f46186b7f428cae4083be61957d28eb31f00a927e2347d89b3132d29fb08b3fcb18ec49eaa57d1152b81a16931b4bd4d5fa26dea56e44f8e34d24bcb5faed41f71bc01b0acf3545b6dafa5c0eb5589e0a41ae10153b5f5f34f1c794c8b1f4685537a2d276bb49efc57bda47b3eeb207caa5bc1850fe23b198a374937c4cb21a75c71ecc086931860550d0d45d65df5836b8067ad47693d005758db0d63db92401edaac810e236079181be49f64fa9a74bac89629a763e90d3d88a45de34260a3c280bcbbf016735116fe1eb798cfdec057bffce2edf9e7ac77dbda6d0847edfe2f443f9d7148abed9fd629b4c75794da07b4ce029a3f7d2aadfae691b36e330fb4418775579dd13f0a8f560f78e40535fcb9fc1a6794b226734d422720f9b21ae82c333e053539cbbea8c899a834819874297c879b73824b48a4e114873cf3e00328f5dd29681726c7951886ab672c12ee94c8d33aadc17f83862fe70b3dc35725816b6c0241308d4b767daca7aaa6105355ec4217f92347cc1cf8ad020adc7f91b020d0a9a5947b93901b8f5e828c3e3c9a36867e750708d7b14bf5d6fe66960837bab053ce7be1d925e75d75715870d2f5d3cb41f71cfacef68c8f5640079d5f716407fd20073d6fc8188eb758850ebac75b6a07fdc4e0a05bd61df4ee9c67f7fcc409bb6c8f736ebe3be8a67b3872ce7b4f3cbcf1f86bdabec01ce4accfe79bff1625fdd55a509009bdd581f1afdd5b47dfd5d8cc81e835e9bcfea1dae0d470de50c47cac05daf9a9532f13d55e55cf2e3cb04ca5364f3cd232bc1e951215a99787a7b401524681b34952e6d93722676e4dfb43a5b421bb5567db501fef50f5c556f647e1e01ea09b6fa2a7bf9eb8021d9f3486cc31722c0221e3a8aa43298fa23068ea918315eb200b0790bcf8965110fc5c4d010b2d240c7e7403daee04eb3fc6c915479ec4d12de83c1e137b2b1715314179c088c36441418db256356758beaaaa1c92a0dcf694bf0a760a6d5fc70124d8bfc84b1db6ab10aa9254467b5c83e447b9b276873b5d1239ed23e98da9033c77ca659bf5c749603fc9df2465f65ad56db5ce87fa442259298af501972470ea90266ba6b3041c436e689483f23eaa0a9c9eb56ef0b2865a07f899d3e10a3f6514a58926727d3b8084d2590e92ed1a5420e34401e5b529663a3ddfb84e9e3c5f4e5fb73ddbce5b5799dbe7ddce7a3a8ecbf636ef5198cefe8379f5bcfacc86553fd907f6647d88c833d6d7045a3beb7d775dd02447604e1eee5df5d2c734f7e8f248f7000f40be112de42d2f4e488d0d12a094310c99a40c8f302f582aa66e44d4688923dcab06482363265830ca067c8305eac4d7a400938a73eebf9e03dad9530b5bf9bd8c72de5f5adcfb4e5cea4dd5c2a5f5aaea54bd565396140e56ac139ffa2210b30f10b3af9f3b87ac1608792a50cd4cc02968ad124c1edac0ad824feab7eaa461c03f58a771807858d45df4d59d29e4d236ca7b963e59d46622578c85be3a3bebfc66231f9d733fdea7eb7d2f3c88ceff697ac3ddf3dd1df4733be856d677d0bb736edc1d749cf8c1f6ea7cfbe98e798fbbc047ce79ef894736eecebab55ed1de49ec7bd6bb14dcd4f96dca0ef3f735e8db7dc233139dc0ea7dd359d554a7dd45def2a8b92828ac7879b8ceb944da0f45e838d5e859e7c696b2cc35cae4e35c71c12692451c0d899824ce0bebcab424767ec65701548ea0b33f15ff8dbe932e654fbb835e1bea42f706cead67c61d39e97bfbe5a01cbbe9df84f5f8e10ca663968463978182c4983166bceb0c606138b4ea487d8a73beb59447d70fc75dc7b1168042a22c8e656d1567f875d0ada5aa9431eb0b4a631d5112e29ccd4641ba2d57a7ca992cfd12378e3e9c9423fad4e92d6cfad9e58d4e87887aac5c6d33b65e6225928b3a693e39379f4a522299531e5a71cc1758dccc23b5300ca28ec84b9e8b72a76a09808516de393ea3fcc0445546f9673e00e4dba5c0727e3e70f88aa80ca9abe415e63c4cbb28cbbcb305d267d2594e7bc5b33e7959a97c022322eee32ed83265295e964b2f92bd5484401083abb2c84fa152d4e016c8df8e8096e7d2be7e6d7bedd520269c6fcefad8ae7223f607796016b973b368bb684cfc20fb41b87f3691b6d3c46780f09fa76175d9730466f2d802baa37e1877d55581a3f028f480475e985acf76fa39d99c945d39b33b629659a9ce9aaad8c90c6e94ba26736835183124c6e060c8f30b774554462734e124c35b73367d74721b01be0163f10cad4cfcecd40e3f2be0df949645961a18430c55125c6230899b01cf2e03b892696454b47e33625a7e6143c674ed30549b3aff7258829c36c8cad01dfe12637a178dbd6e3a5d7fab0f7eea7cf7b0ddd23821feba6affc63f1e3cf972d07b59fb228d9f25e268c32aaf59c43dcf1f3e998e593f83ded90f3be89c19484fee77d045dcefa077e77ceca07786dd31ef71871fc58f7c0ff445e559d75ecb7b7c26934f3e8657378e7e1449183af419b4f7956d72660e9652ab99a59dd9c56bf340ff4a5dd70fb754dfcac9548b75d76e19f321d4ee29e5625bec821c3fbee64e9c01113108c16572ba612f6f3fe2868e4c39192466abe338264f5b0d0f957a52ac024efb46db55df73fca597f7f8e8e84bef89bdf1cd37fff86319a137e03a736ed831705cc0a1f363371d10f3641c8f72286361495b403974e54842a3a200886def31e5ea07a5e159fbb9ea10d45eac50322ec99aa4beaa9f72cb282815b3be2007477c12292f014982e8da14dd6db4328953495e3ef29667fd858314c029957f2b0fff968de36d81b225b6807e491ebed258228ae5c991a64fe2b8761ae23e3fdaac80c6d79cb69d6ef9c1ba6e481a6ee42f8ef2e66503711695c9faea98a97d14ce69a4129414c5b7d6ebb49e02d76588115a7ae9ea26c73a8acef14a21b1c780eaa80c50eb043ffd6d8ac5b7462750a8293374012030695f2a9ded8a1e914e7fcf56dfc97ac85b862a7c29ce7aa779b8e27ea3af6d5d5b4c6fb3efed005b54dfec54cdd5e2d6f796095621a6cb1f30c95cd9b3a32eccb3eae3074b858dbf09b44e61176a50d38ec223dc0377df7df753f0865fedd7884e3755512756075ddb4c1498c6d0b5d69c33b30c54924d71a151ab9b058c82c77634e50650f6dfbc3cc2ecf464397f63525e70317138d6207b4979e8e0c9035c17081f544bfde26a148c1b2b2260200be4e39e5d4d28d2c81e63263c3c4840a03111dd2a62b8c89441d4e008b23f280d7309e469361412f1a126e966d30fe0a05c375d5bdda9171e4f9c4b9c759c74fdf43d0e7a20eb3c40ea39f4bd650b1cf4c9c6467b0ffaa6ef9a5eac9dc341c7293fa983ce2f994c4eb737b8f41d749df3b339e87dd7dcb83beb478eb9a371fe85eeac77233f76d6d1abbc46736d6df3cda8e31deab4bae91c70a3d0e5b61c27c0ce114a84975aabe3e89f73badb0017e8e87fcd8338efdcbd667d92b3b8f0204a1cc780bc8cac3bf826524145600720cdf0016131595ed5ef67d1f27ca786adc9e277b65df5fd8bde91b34ed7ee0b274f5ef26bf4f9931d8b04633e8e83b6cdd1c8f8645862915366411c5f0af38280186de908318a23b2824657c257964d37aad6aa3366337ce421a362a51e1480446c37722867c76d0206065cfdf426a235a5ad41d6a1d0935b41783e1837f07cd03b60e7d1c3e862d15bb115f2890d6f6922fb481cdb10142ee6b209235a03e7d817b4a5dfca2171c953381d068d3c42c78a024ed501141a67a0c7cef2663479585be480a0ddaf24dfe76ab183d6190d759b88114db9ad0801fc536e2ed45d7566fc05b57a7324071c65704d54368fb6d8ff8ad2a44b5a1c4d42e05568e3f319d646e88300dcba29cd6748cca62f65579977ac5778b0cefac33da7bb3d552afae4cebac1a00f688e2da8be3655edab6f785a2fd366f4fcdbe8d32bc56ecefade5df5b6ad7e1877d51de3a3f008f7c07df7ddfb5e14eed94e722761574cb3514b630d831392e084f7758b65854c43e3193e8d0a5aae03105ee04a9141856fbe5a6abb32ce75e1eca4dfba9a2f6e241927dd08fed75205bbd2562956f10b5ff9f3471500dda9305d932ac650388471b645920952d4195a525a9ede46a553406583afc9e122596b87edad208eededddd1dfb6ce83b093d5eb71fadf5cee79773bb639d2427a9d632d38ecfc624d7cf7c64ce7fb0c07bd9565d7d4b40efa0e475d8e9d3ae52bd1f7dc857b069d0709c3435ccea3c759d34137af733e3eda226cec8477e75cf851b8707aa0ef04f5854ca7d5d780f505613a5d7c230ec5bbd55a171de78541dd8d82676135058409e1dc50ffb3fb2d81732a24996532d885355e35376b3e8484c9d2bfa1ca94052fd34fdece4ff310256e3c28880cd4f70ebe7d7a457fa391a7bcbc2d456e5e13bfed374786e1d70efd4649c0d12f95da0b07879b6eb8e1058bd5f2cdb15df6b703e6a062ff620b6321b5d9191d873c765c85898307202338d84aea1127aad09c19707502635233985a66e8ddfc88aa951dd6d1cbf20166e1b65dda280e100d3b2521b13e137ce4154f1cb9eb38c56e5bc5569454040bf8dfc1f525b4a7eb4b9069ff257cae82dd7f01fed7c2fbe9547b3c32594904b20fe6ac0d5949a0ab3699f3388aa56a6a7688536bc9ec0d89edaca511b949a4ab1bbe02c4b903c775acfaa0e2b0115eacc3db76db266f0e9c8fc9d3971e85b11f322e300989e326acd1870fb4960636c8594d746ef69b8e72dcc584af4d69f5294cf4c5bac3c7abb247a8c048118456c563fd520637ac1420a58592762f57b3d36c64fd00e3f3fb7230b87eb166ed60b7329f9dd7fbdfb3de6d5d513cb4d731ef77bdeb5d8fddde3afdd9741e6dc8a88f3bd734fd5203861ce4332eb3c92b7806ed8d4a465fa41d248db356d31e5ea5bce34b21f28375f7f32d283fb2b4ec9b2dd25d88c16973141ec11ef0c80b13f7d95955331533d3a2a81af1ecc2392a59b39d79658c3295311486e83333de33aecefd0a35e93554d9616106abe74eec7c6259f8d98939e7e27535fdb097c6cfa25f09c2d5d613c311868da973c5fa41d568793c45831be1345cfe998fdcb58838e1c48fa41a39eb0d0d50d2524594024a1c83abac328b2112275ca40fb8ed3264717980af367fac9c7468f69c445f67377ddb73e7db0fc6496f8e7b9c741d748367d127a74e6d8d9d740d5c76d09b93ae83ee67bc834e396f7839f7f9f3b1d3aee447e1c2e881be13d48dbd865fc9fb62c037b17f809edeaa2ec7098f6eabcb51ea6a244a9fb9600e38b91c7d31d5543dba6e7195c25fbc4c24f91832d9983fce0e3980cb9cd10c840790012e36f3d2b9a918da16e90a6ff954ef6081f53b5c72158e8ebff49e78f0f1cffeeccf3e8537bafc8a471a0cdd269a70081ca98c983a90e1aeb1132a820e6a8da88e6008c2c73193634a851354077135a2a9279700ab84bc3cba1ef46f44a32b8d36eb83e9f0a8fa220b30b9d76f0224c9a524b064947c00497e0af9f638e97e37b39aaf4e7207fb615ec3fb86e56cf923d8ea6f61a25c3b9fce5e8b601f516edf29a8d8e985344b194c74b9914104c1699feb1c9ba7f46f76c4955b61287745709ea4efc10dfb26ab7323cebc9c5b59685cc85a907d9c74f828414a944d791c4f07907ff1924816f98805066e6c5e109ff0431037a91c07d9842f0972e1ab139fb55a1a102d51f8181673c0a48d50326d25a6fa0d43c0d4dd44b428f2645c79c6925db95fa6ff9e9a022e2c6f79c014bb956fcf58ebce78cf7ab7717d53a2d33e14f19837ebe769baec2e65b799b9a5246d3fd8a77d341c07db693e47052793e77659d0bfb4a3e70f8a0fcb7bd5a33f0735f008f6d0f7c0a73ef5a9c733ab5e513bd5f04731b3783acb99a41aa552cc686e14380603d43cb4d21559d1589d21890150b9fd8fd283d37f080950e6b91b3b517edef4c2139277fa5d773e38eb14bd8c499033951a83be0884509eb26ec44e197996f134c14718e54eb4940151a9821379a5d2d0caa817862c14c18b80d2000f9257a7a6bc85910787a9fa11cccdf7b33770c68f171596c668ef2ebacffd61a0bcc31ecea99b07bf1d738901ab874537764ece66a7b6fadb2f74d0fdf45df42fe6a0eb88f74f3fe2d2e53a72d27b4f5c7871df09eab12de86f8231adc3ce04fd491c201e6a46fbd159b5b5961862945815e79f1bce368bcc383fdaa4cdee9d84ea3c7345bb906fb7c4915ff0cba57366e4cf3252290f5dab51dc366f48518e23d3f3f29a4cff33f4000a40f5bf5a1390f0e51e7fe9f4177bcc33e8bf45a73e4ec7da4f46c9314f9f6bc96a9cb4aee662eef5c84c1888623d73c7c5886a2b0db94b7384a50fa0f4c05ca36d980d41fe656b85d75bbe2c824710b1d2ca4599fae3eb6cbb2db7b8f4cdfa25e6629ca46d516e3eb3d5697ef6fd2769e727aa74f7eabbb1d6f8eca0ce8f62e10000400049444154597c2b3999afe6dbf8527c567fcc0dec6f10bf904d509fcbba7eb55cbe874a1e2867d5a32a71d55a95d507a5e34aa6dc4aad48f66eb527b22357ce7b2374d69d38b8f466e4950381741c79e4af9b29a1b4dd4e1146b913247c6dbc3c82c2453efd4f3cc5314fff38a74443028d40c522c03370c7cf6c65a82f2d129473f125706a058f323ee6ac53d1826f9d0d562998891b28fce1d9d4247cc32dd582339b5dbe9a2cde405fe4b58d907c51675d9cb19d33ff5086ce5ba79d66dce998a5bf185f4f00e4e62a2da14b6d9be5199bf4827a7215edc9f1979096b33e1c81e1c177ceaaafe5bdea7ee329cefe637bc22eb4907ebad084be50e53d75ead43f62525d1ea7bb35421757038eb6e6cf64fcd602915555c9086f7132813b239dd41a67d05474fe6232e2d8a6b82636b3825799fc5a4ea4d6b154ef50bf8127fa799a9ae91d2352fc34043142c00cf2f43f77fecd6e48d38a456f69a4551ec9bc58007efdca5a18296224b7a89892a753b2083498b2689fe064b5249067367b07b6f087a7d3b54f0bda0d75ba65c11b5d70c0f71c7581668797bb483bc017f3c50e4e8a8efa640d02e4dd72171d9cade3ed80eee99183ae93de1df4cb2ebb6c71d96567bec165bf733e3ee272e4a0ef8ed4859aea3b415dfe6ef8f73d5cfa67b810efd3913064c18d47e47c24cf92518b6fb41efd7666abe44e28112a199d7772e42d1e0d3798c0fa8a0ca3cc9f4c52089b6330f0929f95318fac4507cc4f37f694702fbaf357c6dbe99c7e89e3ce3c98f71b55d99c2bb8e89eabfc6228fb991bafff6946e319daa91a517a57abd5c6de11d014e6b8a24382f7de30c838da0e2b5a43a297583e8c25993acf5c202982a72ea004b5532c176db694c09b07a9e6454f2c6d096f24a28fea0721fc940fdab2b91d06ad55f0a7d12e49ad65f50bf0ba3dc4fb2e2a908655077dbe5a0d36778cb65cce3e8b03f5af96eb939be6b3e5b7c1f96ae6c2ebe0e9519a92cd7a91d7dd7065726f39f2d9473ad1c2952bfa2dcc1ac8f39ff364747e8e9d64e7dc7ea17fe9e0de0698c15b3e90c04bf27eb5beba1930ae7ac44bdafac5b57fe99cd04920b4f5279950cd7840551d10d5e2acf9646025b3fa506ebd694fe845b4a61440e34a5e3712dc1c05afe6b23c652c5f62db43be8d6cd658ce10514a45abf9d3887e95799dd7364ac3b2b767675dd8416f767a38e6b7b674e03b9b7cd4be2da780362463736a34683a436c1fd827ea81edf5b720a6df467b6aa381b7b27db1f09f34845e6f8fbf18ddf9546efb8fc223d003f7de7bef5f6336fd110668b3efa8ab903aae99b78c44ed863021b53cfcab9831102d9fbd0491336a9418334905d5ca401ae3e5e466fa270e231016d3d54756d3f98b794032dbe9fcb4390f5e4f7f0bc2aff4497343b1cb15323828430c12b708234324cf88c125b1798429673fac8a9b0c903d9290f44623a2532a9dd79295fa6d478c9460b040901cfad3ec98fc2a96f69f497150d041d7e918af0cb593beeba037ba3d67d1bb839edf4e0fc271cf9defd94117ac833eb99b33e94f38bde70cfad8091f3be7adaea3e810f680467ebcd85cc179f5cf0fe7d5b7bf8585e577d4f1ec9c3b879c5a995281a2d651eaaedc51f55a97fbec0003943e83cdd4ec2c98476b84158e3b9dce216020d53187e21f931142ceb29b215db34da014d39fe09cfaad079d53f70c2ba19ff9ccf106619e693d3aa7beabd437dd74d35f5f2d773e485f3677c1b1a16f6b704074b4bb1537cb18ebfd1acb864ba90634fe62b3e3e25815540ccd2746589a42164727b6f81666edbcead080c6254eeee0f4201d40ff649635838becb4edbc621418b4e1292d984d04b0c3539801e7f79ddc67fc4232075cfcf6f100f000824f3648064033daee806abd17d3d9156bcbd9b722c47f07ce55690b12d4ba800c089dde8cbcb695bcfd9da651e2624ae87bf3c2f30d0274d93d37cf9fdf3867dd6d7476956db60fb8c0b822aeed9f3aecf19455dff73e89539ffe712d074fde0a91710e39538ffa0b6841fad4be8d8f6d5d9653e2b5f8928267f2b9968099b98ad4658456c2e89478c03376d6852c112b7a437ab97a0b3bec3fc13c1e9ebb72bc58ef72667de7caf61ccac9ff2863d43727babd83e3431edefef6b7ff38fd77b3ed6cdadbc6d49b201a670779078bfebbbb6e329dbb9cdc465bff47da12599d7c5b755e7d388ab5ffac7aff06b4b7eb216fccc3cc30eaf330d771c49e1e60aafc630c01e7c69880cc260d87cae73cca5c5209dbdda33aeac4cd25332fb9519e24b350d32e2d8435491b8d064a8df64ebd1171d465f69eba05ad2b8efb7751fb57c61c0c860f2925d5e0288b33bdd76f9c4f6a2bd1d41e5014413e450c9d46a28ae0d310c86bc8844b613a8412fb1d71d817ad209921d669daf7ca7339e938e43c39baeba473a79d5f171d1f81693be871d2d93cdfb38b9eaad860e49719fd8af6e4f8984beda05fe679f4e5d8493fd70e7af13bba1ed61ee84e7a37f8be09c6b6a2773cd0347f3f8a7cb733b3dede4141164ff5dee5c88ff35e702dbefd478b6207288f23ee6400cfc9edd4a869c8724f9e6c8a84c94827ad23657ab6b994450d8036c2f92c918e888f83bb08f270fad3961c7f816736d6f71f7fb11a033b6d65302abbe77a21ee4ced69c05f2273f3cd373f1647e1b719844dc7a99b336d64c6265deea5c6ccb1d3a696b32e98f1c8b8303268907a61c8183aa6aa003037409226123fc31d5ac7be8aaca1a87baa6df6341ab1ac6ad8a9ecbc652c3c7fa597d65a5ce429571194ddddcfe9ff6ef941e16c4e3a6b08f782f5918e4309dbcbf6595b2db77d43d7d4a7ffa61b7ca5b3ba8789f41b74e0d548f67c2a75a7fd2edb1951331190d36e04945de66a40241518071c2dcf20d8e8245957580b6d8d08e2d420f5394899cd0c46c313970065e0d9c9b7df3d5f2f5e0d14e5f3e0b8567687bfc4b5cff95805185e3308a6c07597dfb18c4c5c7d256476c7ad54618055591bcb11a4ea875dd6779d58e50c42dae7b73670076005a453d1f4fb00bc083b75c6b760c0e66b9f58db734ca4cfed6eef64ff5007e4fa33db681f4756aed1f154442b3162c856b61414fb276d9d4d9ec137369753b6a693befb82d9837fad5476fd5bd0b0e6d2dbd7f3e77b9cf13ddf85bcd0e56337fd3b68c3b36b770d73c084771a3919b5245e6b6e76684dd2ca811143092c805686124ba7e60a712e56108f4f0af5213418931318e97f93720f114ed6f81a6c768d4b76190feff9992ee1013f68e34758830b7ff080c148fc82541b920b586e659262a0ba9112b9842156022ec16f135458ec9840ff1542e08a8328ab1f23f13e3e070626aa3f5b142f5d6f837ceeb0dd61ef043ae9e403d749c721c9311770b3b3e00efafe5d74f21c7161179da093ee43a2fd41d1be8b3ede41efb05ee7517c787ba0ef30f5d896fa660163740a9d99de12675815e6e32ce97f9907cee5780a9064d232ef9c782ebacc933e156418a64e07765bf38d55e306045cf88800d9c0afa5fb9ca4246ccb23105ffe32b4aad5d392f8122fe305afdfac7c892c0e05faa993275fc3d8d2873508b1b98e8fe3cb3878898344777b7be4b5e39acc983b8641a5dc743e623a82327178c989ef1f099dd494759b2a0ebad34c73d5ef2eb9776a320f9d447235a7e3273c85c9575db9bd143bb8eaa2f5890afe09ceb39ff1f0a85c0c0739e9dd39e766669b03c3db3d165f4fb17b8babd5daf66a8d9fe9e1a84cd28957dbd0ff0537b3bf42bf7e17025d4d0b6e81ec3e2572dae8ecda5fca9b9d6ec5e6534d7616d5944a03ec43cb58d4424b99edca8a6541f2b6b7b539b1e9b43d4eb5046256ddadefd2918e9b52b882ca882b7079659a4756ea4fdc7888968d30126ddea7dc1a64026db543b9fd54e5d69fb4bc246dba16319a132b61d6eece837ccee5c317104747a75f8b9d4af7b318d6af97f28e616071d69fc88f9bf539de9dd9877a9e77be2cd77f5a2de9571bc8874b35990c536377c3d1ae4d074d7666936f42e6351e3c5bf3271a571b79b034371b72f0acba317a77e00f203dd46db2ae8733d8eaa3f030f6407ba8f0e654e1a463e22c35ac00748f9b46e66b9d94596030ce0765e5dfc9c83f098d7ba34bba4dea146930c807117cceb9661765bafa9714e76ba11df6d6d985fb3e502ef701a1faba5c31b23f0f5a5309aaa8eaad0ba8338790da9cf4940aa90fd742a34df0096ee1cbc3dd8382816f1a87217c425c786110bede5f2f1f80ee7fc63f39eb43a3d94987b7af61f4c5e978dfe5a4cf17db5aa1be8bae93ee11173fdd49a738a13be8679e43af5d741efe1ddee4b27f17bdf3388a2fae1ed86fe05dd47c0bcce8bcfa6fa2bfa79dcb9940a877161de71a13d35913073b13da79a093d6fa305e80ce84009d6ce74d661059629322cbc7853ef3a5e6bc950455f743474ddcf0adb9578e642c076065587db5dcc6617c4e7d0c3f57ba2fbae7c2396c6537de78e3773304d7956d6510f4c83236b4d4b1cbf8d0efc06ac4c111c61839028e455c77c7276354094a8a06741db8fc39c8f0b7cc314f2a34326f3762612306a8a2ab70596314aa8d79978b7c646c3444a80e35419273d4d1b3925354e15cf805ded9dde2ee0ffb9d7436cfb383ae63ee47fc7ed602ef9b6786ea439de06d7af22a767b3f5f6e72fc86331fcaeea0bf7e91232bcfe3e0cacb90ebb6c2576edb482839e3d4297dfa1bb0cd36b096643e8958e366832ddca54fce711247222e612f13c738e32250b830eae7231eff8127022f1cec57913b302c5cdf94413e96519b51e32323c74fbe59332d0b42c1aa126b8387bbcd94db070e9b55c97be0a1c0d0e65cbbf81cd102efd58cd9719cdc5d67fde4e69abf07026cfe89ff786dee9b52f63beb0fe53cd786ca6f3ebfe42f9455c90cdd9e95d8406d4afac1d2ea27fd14c37c95e35149bbab6e6871da65be8e5499aa70c517ae700487f050b66960fa3025e88aa3f070f6c0bd9fb8f7a5e8d62f66c6ab8f5a5215b31944d31a0ed555335c7bdb6db105aefb6cb1ca9cb47419352eeeb4c18ee91ac35f5c04b7c91ffd0ff1d55470271fef301f0fbfb781713c46aef45e89ac097e6d91b792d455715839739c28c4b560e87c946ce28e8d56350f8ec2f9482f79f09c890521aed450d76475172d7a195ef5810b03c664d82d0fb1671bf901a3390e7a16042e3ae9731c74f3e5a083d176d08b263be5079e45ef3be81d6fbc5b3ede45efe547f1c5db031afabee8d80bbca233c744b6b7b7fe056be7333367d23dd1fe36576a9e793638f33873a9f72178c0b33767ec2472ceb46fb572535d9328d3a5fcc3daaf8d7dc85c83454dd94cac2ce2da09d8e81bc8a33b8abc42efd993f9f2b3ce1f9c82bc7f98f992f7a96327988247e7d4fbc8f4d85f1f3d75f2d847b1514fb28363d9ecf0384b3170c0caeed5e0f1f35280f3ce7ce071d0c0d7a66ba707ffcca1174f8e14c61fc14cea6439b671daa20c70054fddc81b86a02b6b4ac202fe2bb8a3ab74ded045a2c82137f9f9eff12b31ca2124ce8d0070f1d326394d6fe5736363ba273ad349af33e8dd41dfc065d2310f171cf33dc466f4ac38b7b0d93d2d00d0660ed1be3d47aeb23dda2e3bd39df5f972f21558faefa52dcfa3bf1e979b13da54e3601bd27900748c7bdfd856402e9a6977b55f0c285aff5b52f8e9ca76099d58e99754048534eee84393355764a8a9db5448830a26034aed8a470125fecb4b280cfa381733398b07b238889bba84255ff5db8ad4671bd5077972714997de34e0b0ec304b90f9163afa065016f477964dc7f212ceabcf2ebf3cdf24b35e2ef6bf83bcdb3be81eb2f08eb7bffd14c26f46e0de5cc64779edd7c89bb4f6d08ea8b6033a4dd73d93369ea40d3bebcbf51df5899d74db9336ed3fa77ee9a5972e3ff9c94fe69dea0f475ba8f7610b35d60f1bfb8b9b715ec7b8e455544ec6cc9c9a566aa113dc09e8c569edf4cabc15a43252eca21a6565a1e5878a0065aa534c5a1478c4bd773636320d46bd82ca3a34023e4d5f4eba383c0c7e356438e956e3c58f2501c429909b2c8babd72010553a64ca4e5647c052e530efa25472d2b2180f5a0f2c3420f4d76389d3e1e1801cf400b2ce7e688f933e32d7fb9d74f038d632dbe14d2e38efac0a4ccf38e9bcd905910e74d2dd456711c85974d2791ffaf82cfaf8888b4e7afb466472e4a4dba34761dc03dd49efbbecfd080ca7587fdb79d5e746e60dca5e5346cd3730eb99abcc18e68bcb1269e68b76210f773b2f9d49d0693fc2431b60b24a42530b7001b507164a91d7d93925ad434740be9438e7e3f0c388f82a980ca1ef40c167f80ad9c2a373ea43174d4e9d3c7e33fdf3247a9560bfe2ecd51d53c6a6862a2346b93103d2dfe0e3e0e55fa7833102232a202b87c90be3544e578d953ae4439fd6951174f009fd1b4e6b88f30f0c6a2ee281ad01571fa2748158aad645ce38b2d117f2a91344eb824bdebe010cce77e160fe7c08f75d0e72d275d0fd880a4f7f76e694b11f61ebeb9cb5e032c44bd3eb388beb710ec5c1c6e71923f8fbfadc5dfa5d4f7db2c67119defe856cd35721edb7cc67d31b68ed1d34bdda9ff65062fb1c1bdb9c64cd81aca9d930b3c83104d77e22a40fa59720fc1ab0f5ebd09f9415677a297d1df2d039cc09c4b5332ec02925cfe21b1c79e486a28a4a7ed3c5c0d1500effac8c2b79c7a7a52da1dcf6042f8d28bd2a8cc20fa12ce5052e3ec035f4ed7f0324c1b73cb196ae9fdcdce494d203c3dcdfff0ef26eef1ad94312d1f75f888db3f1cadf3a4121841ba2c238e9b6bdca01eadccf26df1a042f9bdcf1196da6a3b3abeef1176d1a3a99e32ffcee494679eca49b0ee1797e69ea799e4b79818ab7b5c5eb18a7bc8e11f99dcb4c31d7e54c18275e2dc2ea5e53401433b8e2a7cde44808d5c4e41fdcbc8eb132e1ebe4936e98e0851a03cfc2fc7b61c58bb350d8c773dcc5337ff092b1c68c888f5fb327615dcad1e091d14c4910995375268e5055c8c55ff1cc99a849e5d756c2cad8288577cab605fc681e29aa6d4763ee984fe72fe1e1ed4fb7b747e26843d2f6610e72d2e5e84efa463bf1c81d34c67dc153ec6b8b651d48dfb393de8fba48a7936eac934e7ab9ff988b6563e77cbcb36ed95138ea81b1c1b7373c02a3b3ce1cfb50cdc5cc5ae6bc93490ce78737e4ce3b62e787bb44169681c84e6a509d398dac8e2480279a6c62276aced5d4ac7914e78c62e7582d72c543dec14bddd20789cbecabb81c106ad13ba020a0feb5b8997e937236dcc304e72d2f7f831b9e1739543516f4ab672ae250d0e78c6bffb33c3a90b1e6a6c831938e8ff62f4901e2c51613f712c083f317de2e1a56a30248a94a51375ce274ca84b272f425268fbaa807c127dd6f0c9207572e7e17a3f629b3a85e4a1dd38ad3dc0cfc2fbca63a47262dede16c4e7a2fc7deef3ae77a807cb0df5b95221388f16ed8de9e6df1abb85bac51eef2e623bfeeb8e36b9df2995343d593fb019aed8eeae41d34e17b5947aea609b7f28902dbcfe962dbe6c729943e94837d4affd19f6e95d91fdefcf81147886b991b4bfa8f9277c731df1ccb3bdf54141d3912d5f7f6ae6b68bfe90ea7d4132c29a18d30a4aaef338eb0280cea8a2cc68c45b14502a5306ab1f3d932c7bbe992c759a5ad3f701950500612ebab07d6973fbfbe5a5d46ff6ef856e2c159e7184c776c010fdf12feffecbd7bb46d595ddfb91fe79e73eb565156152f217615620ca28e111148a238183dd241ede123461d686c89f868c5f820ad8969d38d295092d03ae88e26c34e6cc348d37f44c6800eed70c468903411352d02620b7648806248d528280860719fe7ecdd9fcff737e7da6beff3a85b55b7aaeebdb5e7396bcd397ff3377f73aef97bccdf9a6baeb5bb437ba5f59d8f4adcefe5d87d65d491b7b35c99cd93ae94fdaeeb049922c717fefc2551b8861de4a72de90d8f672c3a149efce4d5f6974dfb7d08f92a02302adbf0488c403ec7b89c7e570450e5d179a5a10cb8da653acaa5a147f072e7aff150241142912de211b8f37c2a4701490707c105ee5da6d49ca415e418da60d8e6d29fcafc356b18303adf43fb7b69b6088a4349b599b4f4a02b74304c3ea617cbf6ed279df33acae9b67d4d5185a241991dc182daf5b4605dab061bfc22492e6dbf1fea3f0ceab95861defff7cfa0b38e2296550e2477ff41d3491784b927ef8f89da64de183da01ffc7851bd30babbcb27172fecae6d75b99c5574e96dc376048e1b813e698d0dbece3a72f71f10ecb325f748b8caa16e4619700b4c87a80a4c39198b5091ca9b40e9abbefa63ddfa5e8bce40feaa6ad2d68f3a590fda92094569080b3d6b554959227578715b5087d3c5097e101ac76215fb5507f00324fa24fe0068d774f19d77deb9cb76a55f70a4c317ae465be788c29ac0e46f8dbe67b04494d7e27526c7ce6b422dac907ae6f9f7c969e5a5208e710428c8e1a7501d30695b1eb44a571509958daefa56a073e00326b47eb77a422caba2d6de72fab3cc591f4ad9e8f4609cf4f8e3b57c7e40bd8b9773d0d4b9b8ef78f7f391e36e1790c9acb6e3489e672edb9f0d4e7b7590b17b0f63f1a35ccb5f65c87e8541bae075e76544afd5c0230cf3b221aa81c35ce32c8f1c8636316534dad8669cc0ef7a4cdef9394f1e2059c58c9f3c7110c5e3a8a4dad62b48df8a56dae0012853df87b4b81dd2ad7ec6d507dea48bbad5956aa4f66d2b0fd2a03289f4433aa2c85bd2d551ae5df872f67416b35e0574ceb8eeeaa9ebacf73076d6fb8d79d7f31e77dc87137323f289ba60fa94ebe322727df4399767dfbd30325c80e3919ba934bafcf3e856be0fef362b03f9795b552fc0c6f9a855f50d94ab32ebd56fc32332028bd7a20d7bc81542a6102a66a8ec60a0b9b3c52664d25636a37a8518142bca1d8537ca671c449103170c1478c3b30ae941292793b7036a2b223bfe9a173f304175fb622533e64bc3a92735ea17e14a032b3311cc94e5a6c0acc896a24451a680ec94308b854393b46df888d82685e5ae9f0cc90f83f6dfb1744e3f7dd5b5c27ebe4ee3c4759c93ee377b7771d2d98fdeb6bab81263c0f0648f9a9474d22f5ce0478b762facfd709165d7fa2afa3bdef18ea7fdfeeffffeb3bc966d78ec46a03f12ee8efb273ef1096f20ff30ce585333d788a2729c54b7dc51931056ae4114262aa5fa45f7d116f5265a199db480206ae84a28be07e7d22d5749134468e88242c53c74429f34184f2864d2f94ce3fa53601cab0ec8c6838e3b8ec7372963f8f596c69fe38b199367fb494007aef3254b1119eb58f7b2ab5c7c9c680d9fdc73acc32ac63eb1bcaaa9377c14265d10ad615cf651a78a163a2fc1a304dea70265cd32a71e708caeb635c2435a87b43b6ad29478dd24fa89419ab49ea4cc2461522776f136e8bc19e811216b238183e337b80780790f0b5d456763cbe0a01f41e858501cfa4bf38bfc2cb476fc9cdb63849d1aadb4b3ad86ff99ede3b49703df09c29bffb83898fc389af14d5cda9b3870d82d753ee2882ee91403cc9852d4f8eaf5fb9741c9b893731cad6e096c7231dc55e90c69a391b1a4dc15f85ae0521ec00b92636d9e3f80a1968434c8d91feae59762d38a9f7c2434def44fb9d64d84726227a45e08d4ac64f2c844efafc5c0d246ae3dd203c8f6bce15c7ec3ceceec2b419a9f9fb0b27ee6cce4606fef14df1ccf34dc9df5cd974bbbbd93fac30fd34f97cfc25578491cd90ee4659a69873dd74157731ceb48fe921f3f9acfffbc7d40187678cda6b90f7b93d3a7eb31fbe6f61771c7e14ade748ce95ee934c3b10d577a04eefee8dd5f8d76bc488588ae64eed4a493f35fe5e44fc12cfb4c029cd885f61259a9b5b8c073971f11a5ab96886b651f63158e04026ac22c120afe96e1dadc9bbee48739ac66fbf6ad6946198b90967c3a5282516de431bdf45a9934353c2a181a8f31d21360a55047dc2a2cdbe4eb2fda0f2715fe6dca6bb6f7a986b1e1d7dbeea6f8aff315187e6db4bbe8c4f57fc849f705515b443933393017b45f19dd05b756d2d79df4dd38e9d671abcb78155d275df8e6d75caea52d2ecf7bdef3ee6170fd84e5365c2523d0bf02835ebe27aba3ad5f590f8fbec131f464968952a5402b88a25c5115f2cd164463d42769a84b6a101975d769d609daaa5144f5595d8bfe834b5eac54b60df113533304ad38bd11978af749eb053e5ff2e8fbd42d1d07f7a9a35b73cae7a4631e1e2f4e3aabe9cf6451e5efbadf3fce52c6b1ec98e39fe164a82bc15817931a3f1c6fc7bfcae54198e92a4de3430653875c003212c745de811aa7cffa1449d73c8f6c8af3a425a25805aff541a8f896b526c0af94f38db267995df1c77f34dadd99636eb89b49e527adbd19caeed63d1bf8b19f1dc7bc475f287123bace752f079e5f8e3606e6e2d1708ccb5a79af96d82d311eec675939edd03f756a71e04607be56c43cb0bf4f7ecd693f984f3f3c9f2feee452bf169df9175c681cf6e2dc6afc6d243c8b8ee9ccd73c167838c740d57fc6d53977cefca5139e2d48e1297c76fea34c7ec8a1f032f5dacd01737bf8025238e3d393d0977c2069c776a3dd10d22195a64fe4d5f6bee75d18a48524319d0b30cd41d286221ed4ea3e823c6eec2e7ac19bbe9a8fa63d69729e95f5f3e777cf4cceb2cf7be5acdb171df6be5fddbce18a39ebb3e99fc40752aeed30ffade369c72e7a58103df21a1c15f445387ec6f3533c3a61074f7c1a38defe72c5ae63d4fe23918c8d7824083f9e69f2ab9fafadeb2f1153f84ad9d42355ab8c647762a3b6089e4aaf62aa6b09da6a48e40e329c2ac5959e77f4aea80435426e1b20a70e6db0ed85fddebfd328dd40d157ab1045977648d7ca8cf5d223daad7e8913caf6dbcea84090b6effd3ba6b66b6fed401c07e865bb8b7d9036862c3b66c856b0ae45f6db3acb7b0f0e967ed715275d9b3fb6fbeea75f5f4977d51ccbcf22cba6934ebbc7aea45f08d1eea4db0ff7a38f5f181536de8b6efe5a0a6c8dbaefddef7ee7cbaea53e5f8f7ded06ff3bbff33bb3050ba7f60f729dd11ff501455037c9eb8867e21516455293e256a74aa99b909abcd44e0d481c082b588f107d9684fb19a463c4a19a1bab709d1639f4d3db67827846b3098e3a5fe638b45eee8e818b274e76d67bdc84e9f47f65cccfc4723ba0f0435b5d4106c03ff30191360e98f1173fe5b2439e068973c18b97700c3c7993977f2dd27e2a2b3622efb5a9ce0d8903b48ac050d4814b5e5252170f8047e18452687903a980d865cb410d11fa07e397af5cce1f785fba55b0bb593041a6572be9cd41df74d2c96751c5d8ba066ef8f29ed0b8ac95bbfd65ecd85705ce83d37ee1027bd34fb9527371419ba6dd98aec35e4efba9f3acb3a72d68dd3b9bcfff1ec3f8b58cea9bba82a8238ed3c477a749174f3214644b278d1db95e9671a39279e74fb7abc8a73c5596b5d2e4cfda4350e185659b8d31396894d35ded21502428b3530a903263243dd2b6488a326fe80a9ebce529b155fb21194f38f61c559f92c0e80731d0d0741ce8da6dcbc9ce4fbb5d84556806c20d302b675d271d4042ffaa555f85d6e6f5b2871a33869fb0ffe98bfac165f2b2ab17d1aecba26084578e9d7e8ea3e1b530625f60d2e0aa7aa5c4d91bfadd613d1e6f7f117625aea3d37ea462c5741baee008b037fde588d5b315a43cf6923619454a65eb0a9cd599286e43d0a04620adc9a19696d625abe0e6182838ef43557482fbe322da0a327994f5ed38bb315408f6d70361a221a0ab3e1a8b226800540e8f260951f25280ea0ed4d4f121b8efbce53534698c42afcdac0641522810400e619ecc6aaf286592735fda8f827bb7905a4d57c74acf8e74d2d9b118544e7ce52546dfbc4e7a7f59c9bc9f64f2c876979b565f75b1ec81b6ba8873ad859de5f4979924fec17bd90673adf5fd7aeb6f77d6bdaef9f2d21f443933f9363d40cf54eba83a8a58b1654d8f0340df3230d1a04a8392c0d21824a272e2b04521ba1827315a563547562637055690847a9809bba8d9976c4b5df9e97da78beb941e9717def0863b7bc5cbab700d61bdea55777e1b83fe22472fb63123c905c8481d30798793d51de262aab870a131cb710fdc3a71c8ac2b094f5a4b2db9f653cca29579027cd16d430c439d25534e589716e793d40f69a8419b6a046546aa55393864b23863ecf5a4dd60fe5370ff50d471c0791e6caf706468d8f262da4338fef2f0f516f3ddd9eeceb98ef9f8b8e5965bd6f2bdccba63e7fdcc99834bfdb04ca16597469cf653aeb47363e13cc0ad002fa3aebe22230d767465959d4577e78c7b71caef644cbf914bff2d47c5f1eb5b882a670b15326a6de04c2749515f205397e44ccdeb2960ac8bb73ae2565004e471e6585a73fcb37dca914f2134242cae09eab7aa80a81cf648ab1163920dcfc3d3ee5304b3cde1e5d6e6e9b6f36f827424444f5b5b85dcf2cbe50bf98d916f14f5fc79b78cc42c0c2beb3aebdd617f04f6abdf6fd7bc50bbe6759ab59f8e4f608158c6ff787c052c675fc422de297ff7503bc6180ecefae9d3a7776ff499e1c6d75f405b0bfdc6630d789565b412db7085460047f016d4ef27143275d037ca15c0266d6574c92a7e5999ce444b4e3494aa84d22aa5980a65f69c22bc25a0e62500aec8589c12e606b7aa65929b4cfe1d5102b5bf293eb5ed50e8a3b052879008ccae6a76421b34a9d8a6ed78c3a1f11027c846c1b5a56066bbcbb23bf1e0d544a1811287201d4fd3c9a7b8a9f861faf9a1c0374e8b19bf643782696c7dada8835c599f639235ccaeae40e7dcc5e1a5d1dde0b16abe7f135b5d9e48a5f1575da4d1b7ba98be9657d2edbfe18b9ff39cb7c382838b3bf35f28c8f6fc588d40df0aa2c37e69397f9fb34d4dd0c8bdba13fd71a28f1e9057c7d46193e8519f58834b3e4a896e539e537409e73080e622c4d10b02f814b57692165f9a9a1375cf335953ad2f37e3ee981b85f64229cb936e751915ac25fb842df0c52fbef361afacad11bf4a326c79c19e2f5e3bfccaa7f690c321761ce543bea041264e9763db0e1725b4b11e85e3909b13c1ba12e1c02a32d0b1b1daeef02b701d3a5192017f544f346e12b24f1a38ec252f490a08251ea6750c89ec9f918e5ae80934a5a3a770701d331dd7c92f517062c09e0f4eba88e68d71ca09abed2e647cfa990595ee80df72cbb9039df38acfc5a9afbcb0d5d1f17b7cf6ec9c1f9eab43871d2f1da1e588c7ae5b79969b8453174f9dbae40bab71daf1cbf30519fbd6fbc1f8e2b4b3eeba9cfd4746ec652c347d1fd7fcfedcb480572eaee3e288323a3253fd72ec093576c4ccbbce6bb9717380f9d779979535c662e3c0532fdb4bd5c330a0b9f4d1f35af11633048a4cf89176e2ad3b7fb6e689c358e898f4b09b725f9ce499d84b0e932b848e1b0c2aa71f56a68e68418d14fc04fcfb2f4e4fcea3f3e70fdd7877677dbc5f5d1ae3c509f30f3e38ead591dcdc4880b10c07ec6ebbb8daa66b77fdcb85078fdc1976ee7d9e664c4b06dfe33ec0ebd8aecf00f3d80c9bdb5f36cbafb6bce3b10d576804ce5f3cff0a069447494d1b1032ffcc0a8956084381d5eb94a1dca58dd404285c395474eb45140c8286220aa5885a1f8507e45d73685b5c04a9a56bbfb800d9df2663f88b547a3a28c1b5cdee3c68a0ca1015790d449c08715b08be8d75024a0cd7605496a915a9503e2110957ee5b19e6900190f8a7544968b835700fb50aa6f9c300697c63b60ca49af2fbb885af972d25dbd39bc1f3d2f8cc649bfc467172f5daa4f2f1eb5d5457ad7d27e74fb7b5c808dbfc25392af79d77bdef56dc7e16ce18ffc088c272d9fea44654b71685cbd5375500a94738967a46e658fabcba6d145d17400c0899ea51a58c6e88eca65016961a5fe1a90285a48847e4acb9658a31c08b5505cdbe60419d6f4b3f565e5abeb6ff739da8d040f14de3b205c97abeaf3e9cf60759e2a4fe49d7cc9a285576d3e27f992c266af1d67ff74ca3d64a99c26255f09750e892ab388bffa1713409cae8643be51281c658336cb56e3e45b83ea75d2e90ab5f4cbc5118bec7fe480a800c6364c5db62152f6f72c5a0bb83cd8dce14eae3be51d07fb9b6d5e71d1db9617cbba936e5a67fb9673e5a0df73cf6471cf3df7704cf6ebb8877875eceedeb3f0183beda66f7625fee68b89cb613feb2f46ef9fd1591f0e3f3258a19cf65371d4bbc3eec28ea57e7c80ebcd2a3beb4abf8d4a7d1373d59d8cd2273286e1550d51c6c9b1c619cf5f639c3eb45c347258130a584a29d49ba20e536f932e3eca87b8eca9eccdd4585640053f07e56e9709ef3cf99f8c317c73c58d10f923b64cdebb77c412edc3aa8e7d68b200380839294794cda6def6fcf472cf2d23a7757263084edaafde57a2c7764fd20f2acc26bb596b880e71dd743f8184f0b2675c155dec375476de4bf3c568f1a7f3e99fcd8abaabea2eabb7009fe71a3857d51f28f46b7920bcc7aa3c22f758357e3db57bf7dd773f09617a5917a06882992817916914305180953229aeeb632e6bf8724f84b2243377e5a9db6a5a4b0405d4f7914c949a03807e8cf474f6ef29f1651dc2f21b35fa3134a2f80791ea491374b12468ff5abb36373ceab52c1d7515c0d65c25322e5355c54c09121633516e18ca3e5957a59a4c7e0ef57a6790364e339cf4dc0a737289a63be95def5a7e7fd7ad2e4c0abccf36ace2d55697da8f7e13db5d74d2257fbded47df18b25176fa66879e6f39bcf65def7a97bf06bd0d8fd1083869d9340e42561ad5a36c8b0056da41421d4775d4a3940d1352e9595696d42f75dbfa5489feb6d5bb40285057cb09a454558cfee902545b99e8521b4042a3d95e266465b1befa8292e1c8ecb04da0ab1b3b5f6aeb0b7ad73df755592737f9c2a4ea49c2f5b5aace6afa5f6478bf5b2b97e0d8960d4b5607383f2a058f820193e493263966512875ba0dcdaf45a7102c8a6497352049241fcdf1d7e21093b6e56057dce9157f2d9190fba3e3b4201f2e92f45269452ca4411b39ac629242177c427836793579de155a0ff3e5ea8b2ebd248b2964c01f56d2b5c7c849e4fe9093ce2afa3dfc88a80e3ad5fad1c9adc577dd35597874e71dbb9e11db77b57dff9603e39b75d873dc7ca0b3de8f95c3be22d91d76fa9655769df595c37e3072d8176f9ace0efe6b6e605faf3ebaa0d4753063e9e03b683aec0e6a827c2fbc2c9cc954cbea0742c23ed1a851751cef028436352d646dab781b7e84ae95e4bbd8602943d24819b17268867df57e8bdd5e78088c8cd8ffe0c0db9000ae8cf8977a418d5c881058217a8dcfa7ee77ebe082951569e2b52d30e60dfd895a7770bbddabd2cb3f2396bbb9b948153a4867ed668dbbd2cbf528bb04bf55e49f218b7e29a66cb1f8a2daf8728aed50a7c05f3d943f3838bdeb8afae6f6976b6d9f7a7ca35cf9f6f4b046603e9ffe4d44e68cda922f0364962c01cb1daf421531d3d82a8696a954958e0092c9a3f038ed1453c7159c99ab2b11d086afd1c084a1a74eb051589d71292d34d6078bdfd00050f6f9b3f9ec4b284871e45d83a1aca7f9349efe0c77f8f6df0a85406ccf5afb45204551142f447a1a06b1ccd22f2fdd40cfc90fd47e1d4378e4a355be4270695f938c34fa55465e66f2eb0159c9f145b7f922f938e97e4d80559a639cf49b5845df8b93debfea623ffacaf9f5b0d5c5ebd90cac7afcdaa50be72fc086a7f294e56729ff6b9b38dbfca337027df2520332c17ba38c9e444d500eb5a9690c3a53fa53ab6240a32ee29354a108856b7d53da08ce22c4ef513f179303703bb6fa5aff8d7688d19b2ca5bb2a5c7a4a8533903cc5f73250b34b93280e5f7e592c76d1446f7cbb8f6e2f2a6057fcf28b2ba5f98127a15951e33be32fbef3fa70d671d277e1d73ff2da9af922d5c63783af635ce39838a585ab9be14bbb85cf5a7725c95a1fbe878749c1230c2508b1ed24b5a349b7da36a5fdf7864c471c02e1abbccd8d59f2d4892c481b711366bee1028aeca53078ad6d8910e8ef9b78b9746df144a7001b8cfd1d7c35690c5b5e4c7b587feca49bef212be971d285dc135c538ced60bbcd1f151c7fe177dd7597a3e731b9e38e3b32ab5cbcd8d721f6ddd66851c2a73ffde9c494ee5f3c73a6fb35c3cda54ebb08edc6f36076f1e29c2d937c482c5f12c369dfe158fc34f2fdcb8ccc2b417db6439881743819ffe82abc70e432ae0eb209e763d279f74b40f4b6f36cc59f38f45e0528b2c25af22c8e28b2204f9506c9d98ab26059231fbd8f3f90baa95d7d6ab211d2e0535c841186bc46912643ad95d18e65e21106f9228dbcff0dde7df9770cd4fbc8ca878c9b77f432119f2242c11698093fee36d8008a2675c3fe605f302d798c68a6471901e4d8853fb777312e5e13479e66694b739be1f53b4ee4a6932fe287c74e4d6607976aa961c6a2c3c41bb139727a90057597d52f0d0f8752d7ed2f1ffbd8c7225f021e5aff43ea113f45f81ff156aef3063ef89f3fc85ec6e90fa9a00a7ffdb8118aa6727bed834694fe04d494b974ac8455231ef5b31e75ca014639d0e02ad3112fba552386d616ab1d1b5a4e2e606cde6e1b386ddf961e40d4b8d26410760d449940fb6cbd8a5b4e08a1d772fae925cd2e51aacb917ffb6d878dbb6d4f5eb269f5fdf3f9f27f0a494fdd8c4a233f66c4cf1b01db617b599c74144e34ad6c77d217bb7c23fd019df44b6b4efaf5b61fdd31392a7cc1177cc19f30dc6f4bd974f612beadfed5a6bd31b99c38f5b6a72b3202ddd823d7075122bc621de3f2d8d465e6852857cd0f6a47d9091532ba52fda0521e49ab478460779d22affea606a75add2511546838ebb536aca7232f6e1ec5c743e8ed38f9cd6e989cbab4befb2573b32bead9d9405c01e76ba4b9c2de9bc9cdd4f5e2a47b2d4c8aaf60b49ead9dac51366624b56ff253efbb0694c881e6803719da78e680c0972fe58495b3252f0adf72eaf064a36f6dd0d6e35440c65872b4254af2c281d126513a5800a76fdd1611498114fa64b3322caae96095fd866adaf16b25b477f772b6f8798ad7c2e05537a84e390edae0e598b708733caca49befabe9dd491736619b4b624e3ae9dd093f296e78e20e4ebd4ebb87abedaeb47bf0d0eaa01ffdd3bb9fc4533fbbbbbbbfcb6a3b4ddae7a1dff6833ef20ba907172170c00de745c864cee087aca1bb3ccf3cfbffb2b9f25b583cfa878cdb0579210fbda7927b8ebb636bb6cfc97d7c1591300ffc6288e88e7bf1c384e9d00227dc0320cf74d2e54d9c74eac853b7c4e8dce7574c8dfd03a6bd4819589df714d9bb6abf30a90b24fdb6610f705649b30979b2a2e32ece9c4f38b305667a29b1b379e6908b7cb271a77d5fdd4a3aecfd938d7d61e2a1acaad3626ea622b75e017d28bf828e8fd219a35c0fd79f3b177be18844a26fe76d577ef8a8df97f598ebcdd38107defbd2edb654afc610365e8d1dbb96fa74e3851b7e1491e0ab2a4c7c743c2f9128440a56145b15aa43d112ae21568b547acbd491123ce5336229a0ee88c5d5e206b7d78f660352719b21210daa77c26c7b39e0078e662fb40b02a5ed613b1eaec61b5be69fc5219fb478ae1a09f6ced644c311b12ab66bf05a1423ae5ca2e26a89c4379af0f2e874fa3fb011803eb5d04c7739e9c20ab09c2ff76707b37c2d4e555bd4ca3a06d42fbb3cd04afaa503b7baf495f4f12aba2df4bce9eb3130c6fc125fbfb2e53ffea33ffaa3275cee357787bed7dec60f7d04d6262bf5525d41b174da562b57a5df9e55b772ced439b0e4a18ae80db949fe28b136601d3bd314a35f8126062fb3bf31702b12aa5ea5a414bb137a29163459ee2f9fb09ad680f79ff8bbc097fa2e1eff32695170eb4bed53ef8e57a37ccd463ff5533f75074cf85b5e40f1a2c6b186943164f057e308d43cb8c3b883504f11b5892d80101f0e275fdcd42151ef29c806b80b5f6d4337a5ce1a4f6069989331a0387e667be700d74ba510a43c4e0eed2842ba82d20e116cb452986924c45cad9cfc03fc98955d86968155e7c1b9c5ae0c8eb665d8ecdcbdf9a52d5f1e15662093974775d20be276975cf664e478efb67462f1ccf798f450de603d1fa7bde36e3aede20e0efbe2e6839bdde2c2f6479c799d7a0faf71cd69a7bfe5b02fca615f2e7707871d1d387f7030fd27fcd0c7d73276bfab3e4597e487fae62012dc726489db4033479a55ff2c062fbc62ace545e67463b9227e52a2930a9f65b0f5acacd6b7f3906fe5963459113db443cdaa52b58d94a4fd6ab1f529559529f05a3f532377078d3e7064f1d9ac9a7d5f39b979acb272d677ceedf455755bd9fc646377dad385cb38d1ea6965de7ed8ff51ef1d292f8a12521405c733952a3dc47b8bf9f48be817a6acac19b4864505784dff3fe3d39f14da7f8ff1f69735bb7d19fd7eb45114ad6d781823f09f3ff8c15b0ea6cb1f52cc4b6994a2122ee3a85b694e0451bc0a0a1c13af0a0d40839a3b7762ff63f849a4bc101056b05ce549b93845c9ba21825d44a47f4f2853fe5f017d2f74d21f0cb3937fea6a5cac6ca6893c7455883a176dbec0d2f2ada1aa4135f3f685fad452cfbdb7f77371a6e296c471a0603a7d054a71b75ab333a88e198e84e6a42febfbba7ae99750b6b193ee84c00430acae6078f9fc62df93ee769772d28f7a69f4729dd5de9b6b356608fea5ecac4960f2ccf3e7cfdee9b5f4ed3e97135fabd77e35f6bbafa8d7548eaea8833a50d1287adc26cbd88ce8a7575113963aa51ee68fb2e86a5341b7975b479cd59954744f24e0fe532f41906a68a6c1432a80254e19dbf506b70c1cd40cfd62632ae9daa66ecd63024e7af9e993ee441d8378cd809707073fc978ed393c5a33c72ce34fdc6d2383db0614188369b976ba16e04d5bbb391730ab8f774c628ac4814498a96c04ddd6e24059d7f5f8b4d8f8e8dc90c9da06f15a2257bd0cfc6a33ada62f59f5d54e83d31dc6fca00c1794dad3e99b9091b52d2fb6cf0f048da54190aba781811f631d27fd987de9e2fb551756bebd800ca130e4e390732e7c1cba0c196fe0c751ecb0567ef10bb94fbcf1c61b17f7dc7acffeadb7deaa43beb6ca4ebf873de994c569a7bd631c767fd57a87af8875877dc7ed3d771d2c26dfc120fe04a37836fa53ca0719c611be65c10b66b4fbe482a74ca6269be9325f2d69bca802e74b38a11cc0d3d2760881d357cb159c5001cf3fcbb2c22e7e78db2d067970150df53eb2459c04b05635f59510ef2f6a95de322b16edf4c236f84366fe5b683da539ebd565ce172fb2b24ed0d1f5b060bc5f3ddbe0def086f06ba8744282b64ed19a5dccd9f1e84ffe06b8c05c8465d5ff5ec3b869c6179fd0cc5064df87cc359450dfb7e1618cc0851b6ef8518ce267292ef5df9d60f34d4bbac51f144c85c3b8d2ae7a62c81d3942177814933de6c62a91e520aec858a9550cb4f04adff6ff1f0a6f40dfbe5e7c4ae88637043801003cb4f24555bad54debf6eeea78e7851a1083a7f29288e2b8ead76af7d5bd4c52d054690a9f951cf196b39f9b1e1cbc334e39ead15eafa30374213f68a4dd67bb4b73d25d6177af2c16c03dea1a56a2e6a4fbf48a03d8d649770037c2977ee997de85897f5f4d18f06136fb81f7bce73dcfec372ac67de5bc3bed92e8f01e0beb78a6b7e1a18d00fb37e31745cbd59d2817b4a270c6ea6f39e6ea4cd7e8d2417224bae3a756952366ac1eaa892aa968d626e964c68d80643d2c4ffd002cb28e4e970821420e3d3d98dde622d4f01a69a6d8764ffc001f7ef9cc67ee98b5f749edc244472a896bf4f4aa57bdeab9d8b2973880f2212356035e8b120e9b83688971339a3a4719e636ac4409e2642d23b81ade54cef05b375b0e1a9da18ebc91b11ae946df6a71aead2315e19500cdc282fb24b7faa181d7e68b24aa31e754235e4e3eccd39b9f4f613f619fcb85a91549c15cd7b0e5c5b48770b720f69747cdf7902d2f7ce1a5ada47bc1cac4b062def12e27eef53669bca1c918e5bb7e1654c7d0e73a069df6eeb08f9df6eeb01b5f9ec37ee0677fdb0a3b6f4dcd66ff82b9ee2f735ff5be5a4d473e184cd9d07558e7ba8fb1b1bc8fc3edbceb11ded70ca96ed6225daa3bd0458b4a702e40e77bebc5310de3e41f6dc85a9b2ab607bbfa6299be87e545af9ebad91bca863b896a01347b91c36bf25a3421fc73b26d3f7bb8f8f134c0fd1bf121ddd651f7e85b601aee83fa642397b89bfe23c7b65ffdf022ea02eda3225e174c1f41c82805979397036348fd59b1e84f0498fec7215fdd681cbdfd65fc9946eb3fd82702d679344271edd168e93a6c8317119e80d2fd50bda485a810224624233ac4510563844d79eb87562c3550aa8a290b4e439418d80ab175e38f8b004ca33daed357e631fc67d1a93f04e92bbd7988100ff5350f9091e3268c209c157701e0556f13a52fe208d368a469d2ee97d703e9ab0192b26a2953d1cc182c26bfc50ae22fc515f7d3a62476da64b0daf2a293be8cf117e652077a1f27dd96f1d299136a255d35e3a78cb64eba03734ce086ecff4a51b47acad394fd3bcdeb786f3adf1df6d6b7bef57477d2c771e86c4f0f7904741a4ae7d5ef580168456ba3d3ea8d1a1955e4e404157cefa44d3bb9ab77d1cba16aeb4fab402465eb463f435e072d54a3fe29a456db0401cd865c281664457ded3d2b5c2b7e1e66e7022beafc44417feba4b5bd19959be4e719c7ced526d6b590477f5e2b43b489f2a0b625606f1d2b79615910b0ee8e3530d854b08c3df68b026dbf754227b4e05221820ba2bca5ba647d0131fc2bf09ab3226f83de64c0b6b3329bba962959a3291cbaf934ae6d035fdd1c92b7afb401053eddbbfc99a3b6bc001b56d39193d8659a4ae879f7a5779831267ad8f2e2a714f9c08b6170d275a803b942a7fe2e4477ce6b6f7139ec7dfb82ba3776dafbb618631cb91c0fd661672ebb0b5dfc1686f617bd94dc1f352e869730316c8a22ab56c5617968aa4218109e2efd3554823c5227e5a20e33bc29c110027ad575cb129c8b00521ea8bca71434db52de3cd4738365910168e64f5471522e7e5530ce8f65e1e1d79f8d82252d3ef9cbbcfc25b46b6707679d1bb2acaaa7a176dadc02d37931c6392a0ded270bd79fa8a7fc5c3e697f144ac7ddbe0cbf020c7cf087bcbae07975eaddec59d21907c675c3591f971e4e5fcdfbd4475a7eb8e35bc8c923b0d85ffc0d44e7b39097288302163541b8141e854aa36d79c1392753ca99552e35425ce106ea5839ebe701516ebec020d45d76b4d22238a829a81627ef90040af72d45260430de1a7030a411135a6d4ad4bf7421770eb9576d0a43893e83a8e0d9bfa0903565da7ab660dc2882ef184ccfa2d63fe3275c74cef7fd4bbc83233e1b2603aa2694e3be5a491788213dc047c765b831bf737e69eba4d7609d709ecd767ed9f1ef461b67ef25acaa3ff7842a93a73ce5290b9df6b193de9dfa1e9f547f5b76f4083851a9972a861360cecebd5128eb50aaf555819a5e169efac39f4fc1a816fd12371a57b8d157eb904ddd44f03d33b50e1a0014b43b675d57a3ef9439c1413275677cebc5ed66c33a2a1a77b1b9567b0fb0a20e8521b8ba79a59db281f8a390f8a957bef2eb98d85fa891d4a9ced83b4c49b4c14a9e61cbc0cb23cb63911deca4edaae3ef4aba68859b5980bc0e59a4a20af19ca41c87449e188875a8321f50597dae403d6952164884a7a78dedb7e59c6a85312d8536026079b067935fe13ad7b6bce8cdcc474e7a6bd0d5c9d86af0e3b4bb70e213cebe9a4e6670d253e71ede1d1ded4b17864c3c2272d11d76f5acaf8276474b587714c70ebbfde94efb490ebbbf7abac71e760ff1087d85fd3c83fa1af4e7fb61d2271c51f9971baf3027998c7446bc2978583828bb25e8a8db9e52a7519151f987538250606525d22540be3b87fb473e552d6e7a2ea0cc49952b59212322217da07ea0d48bac5803df20af96f93a8a6d34e4de0ef2fd4ae667bea2327c2622f4bab3cef81cda0213044e9d2f3d7f544cf76eaf7ed6f58953793c206e1e62cb40ca9859e6358b900bf042c489cd7b2ae352b789146f067818ab36dea72eceb5b24f5dde6ec343180157d3f982038e7a19751d6b274a55a1265984485d54f87d1e15998a08a635e165b49b4a5114f98b2a51688ea85645ac5c865860a88832e61e028b32bd8bc9f94b11d8dbb36fb24df6f6c73daccab6b523ecd6b701671ae0d28c930d6aed91a348fc20772300529ead81ab01b05282c6a1c1c8330eff787fb2fc686d446f5f74618b8bdf4a6f1512d1dffd72d221cb4afa4e7d2a2b4ebad6510f9dfd6f934bdbed2ee3613b360d4f7e176e7e2afc94373090cf56fdfd71858f7ef4a3b37e8ce163a77cecb48f71b6e9073702d1ef785d32a3eb1d71e5a27a7a74a8dc70235d4e3418cea1aa27d8d14779a9aaaaaf1650e242401c3acaa2a78255751b482b3cfd0a296d8c70cee45db52b3b15ab7556277dac98bbd9ac200dc2fa875f0a76c4f95a5f5167d5ee35354a0e608d53064dc34656b367224e8de32d8a45499348e9e824be0e3fe4e264a4be635fb8d28a69b50ac462ca1b1f63a3ad2e6e3ced600752d529516812a4ee534e6b1138d5bb43c4a4d36f0bd2fef4c3383587b6bcac2d9d4b02c77ceca49b775f3a4ece2127dd260dd9973e39bc2fdd329d75e3472a7467bdc7b63376dacd3f1887fdec59d40167dde3600f877d6f8f6371009f72f048e12df0fdeb19f1dfca3c98c10dc34bf75440999b7ff843dc544f91084b001640e9bf951bd94249fec4ad1467904d2b2bea693d5d0bb2bca10de09115ea741f403869f5bd64938c3004a0da897f1039034c9faa9d120ce981a7acd18fea0991b0e9f2d9d88d6faed657abea2d9fa83beb7d0b4c77d0fbcdd218779c7ef39bdfcc571f67b7d555d26ac6c42b6ebdb23ffee75a3340ed5a80a5ab3516190f9885157c0000400049444154ea70c55fa02de3d621eb0e8c5556d4c76d9aeefbd4edf766d9d59aef6cbe5afb77d5f68b3bf3ef474cf22ba40a4d499709a767e54b4540d5a2a5c0d51b4b22fc55c14f7d75212ba35d8f73a2c42aabe88e4094ca0441a10d90bb48adb0065b858424edfd2ef9af2a20e5fc1505e2dc75569f044bc2fef49576fbe78461a84750515272c06c4652b4a3615071ec9ce84efec3f55b79ba7837d86fc63d3747601af07fbc411de8d849178b7960df053c57d20d4c0a31eee4b7db5d1ca0cb083ad87c74ed379dba8b93f267f222c6feabce9cf9e8eccc47cf44df9ff08427cc3c4e72d8bbb37e19cd6e518e190179d075592750fd0accb32aa43e81e32459beb77a5cae5cbd78da095b4b4ce819abf345287adfd431a0a08d00ea785cb934644e4af4834930a0c5e4136b5eba8d8cc369565b573f7a342e69e97a9bd415f5be7ff808a4ab1ac4def49731eacf76a4b4c9195c79d5163a1c647d8699f6dad2669b6327f3d8b1c12cb58e71c8403583ac8da42e4cced83b1ad01466088ac6de54e052f0afbc716b954316cccc29e949aadb965b238a72b80a3c76da3869db9ef23b0b6e7959ae7de5c5dfadd8fcca4b77d2d3b7d11698be922ebc07f7a59fcbcba3e96c3adc1df31e77dc4732ee4e618f7b5b2739eccc2dc3cba75cf3da96189df5b3f3c9257efd14526759483ab8c89c746e8f0463e8f151dea87c29c3cb8ff871865bf25339a9b9bef3a364a1308ac7910ff9cc9ff6c1ea49cb73e7559906213e025e0c2cea9c098294b1345434d46fe7630b35f0558d32e51480f4f406a429561048d79fb55a1dcb745d3546cd6f4919df5687f7b772cd967a6475baafaa931fc2933ff399cc31dd59eff180304a40f30ee91bba2ef4be7a73a22ed4b8d21d7b99b1093ae3965ae97c0d05d7365d3e359f8c2b94e1dcfa3de44df4aff1af01c99cd4df4ddc47339f417d341bbc1edae25748cf20373f82b644c88738728504796bab52206c2a627c69c151c01a81081af8990cac17451347098c14868cd81da262650d4cf48e652261792f13c91fd3e457e85547255554db24e4c51292e993793154043a97d63c894fd7ad522b485e43d10a1570ecb74642f4ba7a4588bcd1727a01f3c2cf6ef7d0d66a76584dcff7d20b8ee2b482ca6324935f39e9b52f7dbbdda58fe3e5c738036f6b1c8d8c34497995143efe848fc741efd48e73d8fbeafad659ef23f5d062b5a4bfbfe254a9335581380640a75c4daa6c1c40f0a8850a83834ee5964b1b826eaaaec1cfccd42801cc0fa2114753633f5a198fd7d5715b30d6a78c6c4037f0e8365e08eb4f5989f2d749f146aa361f9136716845fd93bd782dd649efdb11d60aaef2cc6b5ef31ade335adce98b988ea31cf2c898259154eca11bfeb242ce38d62f4833a6559c5a261330a2c263cba1119b694178047d79e99fed594e8bf9ad0ce85b16cb6ac1d03e206d7af2a9101b2c49436e2e74ac30c2d62e3ce8db1e008ff974b9b6e5a52fa3d40f1b850cf5d6f7a563b3eb538cb59c3eec4d671d65d8f2e2bef4b6df20d2a573ce7145f7a557ef1edcf9721c7657d975d68f75d82feceecfcf9ee55abd95d561870d6d759d3a1719608662f1738cdb7f3f9dce2f84d130cd3f43ad84a37f4eaafc3bb986bdf95551f3801bbf758ce39c8a636584465ef6b3f3b87f91d030b8ea677e864656c81534ead79c2d11dba316e5d2b2a9fa480409feb53ea1093dd0024b77526c87093c42a2eddb48fda03f56ccb7e6d756a1bbb3eeeab4c7fd4f7ad2ec334f2e67ddeaf2e138e797c1fb1c7bd646abf59bc61d0b2b13fa75dbdf5a712f586e6204f2efa98664f6d970aa8bb605d64f7ed3591f5bb1f10ba59b72132257c1a971e32ae8c935d40504ff0711e8a76a88a302c4b5a3ccd594a67c26cadbcd046db61410b1d2a4358d50392d53c76a1e2d112d23ab0092f75fc515a98538ecc04ba443f03dbc28fa3c90b2673e989ccab137873212e5d17965cd084de3b58a2e4e15e64d7027128ba91d454e9fa5848a0bb07e667f62ba8011f83f40fd506d7921d57406051e3f59b76070dc75d20ff263137d25bdf6a507a99d6ebaa93ec168f6f1fc09c6f1981c97e6876adf2257e3cb79e2e0a5a02ff9d4a76efdabc7d5193bec1d67ebacf79178e8317ce0075330b1e8b013645406de94223a45b26521137850d290faa79ae5b718d0a9d884a6e59a02755160625510e4fa954120ea630a4caad94d0ed4570ae2380a17d5528485823f91e44a415729e665be72ec690838f19b6b515f98429df4abc1411b7a7a99093ef3fa638c556cb983a705cfb8714e608cc33ff9e2780bf424e3e41d473031ea660d8e6d78614aa60702bf216036ce3e71f2003addc80a54b255d16ad64b61e184ac3c0656c5b468c286d38e34ed8ff8fea051a33199dccb77f6872d2f7a2eae8cb07e7228745b4d1b593c614b3ae1f82d2ffea2d1a3b52ffd50672f03b0e978e9340aeb709df5eeb04b6e730ffb053eeb7841877d7e76508cc562cfbdeb17277b7b7c4586ef26edefbf89aa3c61e7a6b746bfc61e4039974a883c23c61e1b175c040e8b9527e6dbb052f1693c3636945e532ebae5c23c91365f62d6719baf4041894539ec55aba5a91ccf43221230e7be2e03a0f4d5324d4480cb17f3f9ca3b921cadaab7fc10312ef327ddffa410ea0e7a1feb01a925b8117c46b6db5403e94f5da7086d9c288b9fc585dbcb74a65d9757a079cdce040ab1719f2dcaa1104396a7f6ab1bc84d3336aad4fb3d023de6c9c699c7bc1fd74c073ef8c10f32754d7f04f1289951bc22414015163589b2060286f1d76117e0ac8be6986c6a5b70f2c2113b5345a7d188ebdf8859a7b433e8b4ab14a78af17b31ea5f55bf8a2a8c02142ded20cdd5b48a1b2aa19ec99c7c6e0a5a1549aa9fa1dbe250919c87452019dbe7bcc84192eb7e3f8af2fa9a022cacb0f9f2e87267bc2fddcf30f2f095a5bb4c07d9ee529f51e2dbe8d9f23276d28ffa3123577d7b5bdb180b3a9fff010cfa54997ccc2f3c533ef995da3b29db653ce7ebc7f96185a46f87198fa30efb768cc72372f969b564b004635d451fa33f2a19ffae961aa98f2d41446db296b8f26541d919d299ecab8ebdf1f70baaaa300e1475f8f53e0903cba75e8983119db767e4668b737ae9c3323ad8272bd4782d8acfa8bfb7b6bee8a4739c5cd5ae5c45e1d5af7ef5d398e57f8461606cb1e3eda62a638d356664d3db25373419cf625260d9d688518d0d66bcb3e002836a8485d72105b957bc344d8e4cf053107240ad6b1db94dadc6b7aa4f8644d110cb10ecc84ee5a8e5cd60fad8e8847edafa9fb9011cb6bcc403c749dffcca4b77d243af39eafed01c76e3105fddf2e26afaf8538cd6530e5a7ca88ef0c72a6c3a8b4739ecfdd38ef65187dd9831495ccefadc270923871d679dd74e5966be88d3f97f33cd7f2b7cb957de857d30a3d811ae17df01641e8537ea6c700380ff3aa3a9a034140de5446b51f376c945d82a8c632557126e0489d262ec446f2352153ab9b14fdfe2aaa79a6e4b642d71d9a7f4c20924f2ea57c4963f76e9d26c6777b93b6775fdd016189d745a4b78725b55ef4e6f8f7bb9f17479f00c2fddebe8726b5b830f63a7287330b2c0583d44178503b6c394956e28fbb3276bc73ce8cbd8a40159051e1a9e9a60c66ebeb93d05b8ff7e290ddb5e36656555f3b14ba5838f5df3d75ecb37eced7d1f77b9acc0202348571e5d296d91a672dab30a429977c3b52f91d2e080463de5ac3bcc0a60c4ae954770a331a0aa201922e913c894c05247e12d28b1467af69f20fa02fba1825195390824f134fe92b02dd2debca73fd2172e7e6f933a5e57267b8ba433c4d5a26761b5fa94da92fe2738e5cc01472cd3589fb0a393ce2f8f568e3c4efa1e9f9670cb4b8719c749bfe1c2fed6491f8fcae5a5e3544f27ec530fc31bef64e2ec762699ef5da7721fd9274d6ebae968677deca4f715f6f5fadbdc7123f0c637bef16970602f2b62fd46bdb8111d3489ce444fa3abc9008b8a7132a640b0f6628ad28696601dc4180a694864e4f843d8c7e082bd3b2f3d3663a818135053de8c178fe3b555699d4fd5065432d9fa12e0fafe97d9ecd3c384dcbfa3ae937eaded51e741df4f32ce67e21c673c191fff19ef72b1b08b194300dc4c1980b431351690536ca1fc6a681400b74847c75a30a48d7ec6be65284b31a7b2f53a66dae2c1f62b1c0d2514222c1d621bd26e38c47eee51da6eb3a2ebcac76f71fca6357ad0428fbff2425b2b29c0cd1eb6bcb07ad2eb18931d6d793947d93d826d46073d8e798f855d8d61d3093bc9613fb4bace8fecf9437b9bce3a4ea1d7ee0dcd1f3296dfc49ccfa255e96b58038fa2c72065db681ccdc637d2e1b56c6b7c6bf379e0f23c72a37cca6b72f04771315b65c96b03c4e19f234603ce34a942a6aacc2775a9250ec1b6532994ec277970bc5d2c1f25540383d68be6f3e53327ac502353f38da76da1a7b3ee713fabeadd59b76073dc85f189caa71ba70773cf35663efd4b6872dd1482be16dc2b55efda3d44624b28778b0efec364a76fe0e3fa8e74487ca1f493eb6b0e69d2d351371543e16394d03ddb86cb1c019d1584f7c722144d9634537ef333214a80c0b89f10298a8a2150ea82f7ae5dee0a5e04548612326371cc71801c0c4ed1251547702865f349e885267094e3a99ccf582b7e81f5ac132af6014020f4d5fe91d3c2baf7357b67ab381812f6c5a9280269a9f8e2aa754453996dd7bc27fafc3e52bf7378357db5e5c595f4d1f3f5c925f73fe2a4fb4b70abd5f4e6a4630c6fdabf699824308e990cfacaeed881b40bdb5023d09d69e4e56df2c92587c80e99f06ebaf8db8bc5fd371d1eaf72d66f3a5f0efb7865bdd33c5c670b396904981d9ea9b6a8b3d13694a9f4581db7a4e90f89a499b0138364ecfe88dcc433e1c7468ceb80a173a6d59176944358a3e9245b99504c5afdeffbdc33c3a70d7e577be7f03c168f8bf2f6c498d478ffcbd1cf8c71d0aea93deaf4f78be1cdb73b0c32a61c1492003db47dc510878f3feda7039ca86ca355cb93d775b050945e91388c6e5ca18da462388b50484a2fce35351baa375fa102c0388e7b985cf5d21025daed385a9199e2bf750dce015cc7059cbae19da1ce693f95bb19983f2e798736bd544ebbdf4b1fffb0d1d849afba4f73cb4b1c7c9d73f9bf49f36ace6f3a8e0fd661c7893fb7e9b073bd07f0e38f515d56d6f9b003ec2a9f1c79898bd0f80eef74e42d17e21127551bd085a2cdb74a0dec6dc1f25a0c4c3d4ebd281f9610b7cb9cfc57886d469afc8710c422dba1abcc8898ffb491b6c8dbddf26b2cb75d6803444f5855bf8404ed4df6ce1f5e556f1d4da4b36e62ecf88ed35ceb1dad07740101577639866ba2aea0f4d122f28e87613a2f1d49a9d714f09497495948cf5afa6a419d9e1eda5b1f22c79c3665e318b447159c817c545bbc861bbbf589b7fa9922ee029d6c4b64542c57961526401123575562f8151ea5cb52d2196c96681aa86225919406363a2add18e8ca4b5944c552fa915cd3025b4cea0364fe5c5a4f27aa8e79a790d642e80c5f2a08905334a14df666e9448c85b56c8fa356e0bd06fb40ff805954855ed7ec9f277bc26abae507ed855226857d9d80239df41b6e1856773008073ae97d5f7ab5b13d1f3702fd4666ce3ef5ac3a609195bbc80d960cb9ba6d7f7ff76f9e3b776e5ec78d2d3e379fdcf72417d727e79bb3bed986b4b74efbe6a81c9f5fcc78b1492dd13ea82cd1f336c9ca1441304635d492440da355239d4c393a1724632633abaa87189372bc6be205124a22b828601dab25e4c65cfe6b81c0234e195ba478c6df907a147fad3283a7bebea2de318dbff0bdb547fd5a73d6708e5f4bf7799c5f639fcd817d081d3bc71804fd632d7c1bf8e85239d22289a35da41cdec476a6165078677dc7d9b4f63bfff2ad981d9ad9ee10ac4216d38a71b848878ff28f7a692b04c11529de3849cbf45468478f30ba4f92cc3fa3af1f356508a771b13657d3e3a453ee6739e96bd0c6fbd2ad8b533a084a6d79c9a71827f2ddf26b8dfff6d9b0e9941de7b08b7b783b4cadae77879d71ccde756e8e3eced8ff378ce6af8739f0a7a4899c62a0cc295ff20b3ecad29807123ac7252fad0e30f91eae06df7a91a6c846e44202ca80b1d84596362204ad1dcb4168f4a4d98aad927a010874ae88e7eb62816d877cead3b717f272ed175b63b9eb1698bde1e9dae68ba5e28c5fd4343f1e6faef4196589d271fa41dbf625fdb1a3f6b2c04993d1747af5d6b0ac5224cc4d798239d9bfb916048707f713fa951b0b7461e8ab35c661b39fe31b8a31de63959607db709923c047147e405485b784a4553403247a108d2b78dec64f9986bc19fd2e934d0154a6faa6a9881cd4172502493a0c8a545659c8715299b3724f198afbc7547961296235a0ccbbdae3649e3a9c8bd9950b340ad9268ed668e87a2db1281a1157973c8360a70879d9897c9a984ede8f02fc660a46a76efc05f194697dcb0b2f8ffa607dbc92deabdec2f3a8bee565eca48f1dc5ee90f63adbb846a03bd20bf7a9cf26fc20077ff02c6c935925542f6322e1e9cb7a387723ce3a9e3abe7ac251abeadb715f1fb313738bc5e7595e0e1789e8173a83120e132830354c45720b9d28790ad294be1cee542cde8117271122faeca265d296b704cba415879174c022da0a51f655abb729c8e97eeb39a5ada635212db0f7e5fc211ffde8e7c5386ad7cc1e753ec7f80246e4453a4c8e85e39851668cb2c8e2f80aa0cc51f228eee0b283538b30b5b8a1d39dcf36ea60591984153e291be8c42c098b220491037926beb343f1852aea29f6b764850aa2034b978453ac9db6a2f708ea7539692d0f3ee51fa0e497a46cc05349d874d21b38113f6a116ebb983e86bb9a6e7eb52ffdb1f95efab84f573a3d7620a5dd1df6de4edfbf7eb9db61a8c75761262f83bfbf58f334f2a1a0352758c7dba05c51c20ab1ec2c5e17ffc118cddd11a3d480e9ca5a985fe902fb62ba021129898ce857281b92555efcb29074c483840dd649b9a52059ab23938a6c3c06e0f6b1a4b4e48c1bfe1f9c4e5d55375ce079dbe9239fa6e003b005667dff77d569abec533ec891470de954f5c70ed2b974c31b64f3f4c4c894d7e375d5eda87df6cf2e0b5b4e4e2d98db3657d487050709ac02379cabcc28b5c9fb51d163962c6979cc9abf761ae6938ccf4522be3c0284646452b4fb023829d87d15a6a42ce293b2986e04a9fe5016eb50ac8dcd6329ebbb3a82110e39e031fe2a946d699cc5694a23927899904b906f86ce5e9557bb290f3109242105e8d81b159d341d58b25cbe3208ac0a811a7497d16dc33c24a45a050054f806277abd45e33076d20bbe72037012990cf62075785f3a3f43bccfc6f40357d2add757d2bb83d8e3715bdbf46a041c1f9df51aa7d96f4748e4b32c53fe64227b72e1ecdf5ad55aa55c65cfaa7adbb33e76d63b56bf19e8f96d7ccc08cc663c792330f8d1b52810797811c55397d5aba6d3a5a216a29fc1b1029ccafe53f1c8aa854ef42293f405d1e827a72ae64c9139e966c2cf8c5cf8e5ec559de6e09f1b3c38c9279cc2c35885f1a697157435c1bd7732bc4c7acd6c7fc0a5f9bb752d8c8b469811d3e1f6d722b5893eb9a00430e7245ae4ca7646b78a2c332f1f0b031af1c6ac0450de4bc264b12609fd290171478863b78519e469ab27ffa597851c8a6a8b62393121212ef2a13cd862f93c3628eef27f0134ac8227d1bd750a7ad056eb65212f41e13dbb07d8f2929a19b5f16a7aa777adc79b0ebbd7d361e31f4d3a6e75bd5f3fe3eaeaba2ff0fe2437c8af86957cc75e6971be2d698988a8a7e4a3abc265a4fc2fc19491854c5df1a81ed98960994fb62a76c950e6221029e7d4843070db533e8222cd4617589a169d3fd3f62f91f854ca02839d984d5f787030ffd2295b6098c7e7781ef3d3a7cb591fafaa57edc3abeac2cf9c39f339e81a8e00cdb83f3d9da3c03ce9ecf16f3d29e9b633f6155da5f381a54ee98b7dd5b3c171f8ecbee985271d95ec2fdb60ccb8e9cc4d455e28f58dd22382fcdeaea81f3130d70208f97c39ba828420490a9392e1e17ff21ae5200490524e2a67a0208146de18e1d2310f2d95426143359b324a47529143f7aab53b4cf379192565524b03d6e7250ada11642b566eb46ca37f3bbd144d8cea8b295b8f01890140d5ed53fa5a582a72d45ab049822beaa9b9987c80ec6f0478ccc917480fda0ba418ae4c06b4e162faf0a346be3c7a032f8f76e327a9a3bef0724c135b701b8195932e8f966f8bcf20d34cc8bb30d0fce4db5d558705fc186c1d7d10cf9dbb71eebafa4de76f628d6732d15937de3ae88ec2e5073e7d7887bae5ea90c149c4e07936dc04536e50e7129c88c892162fb6c20c36a0502cd41e78385f4a554d149b20522f17d50370caa950fd11d12f8227fcc9ca952b806bebe3e5b15a501fbbeb3ae98727b86b65459dd5f4e7f275881785359ceacfb1d4f63142c5ae0c8663147e0077e1a446b39f2dacb19546b120a956e0a83be6453b7c61c1c5dab929804fda63cbb5f7da59db2f3ae154f1b9d12e5d869e28e249296a8d0c18e3d0495d02e0fcca62317da7397df3f8e79c3657d375d2c5b908c3bba3ae5dc6c119eed52e63cbcb585c24775d84eeac8de3b1c37ed4b7d7fb8ba67d2b8c03a1c37eb09cfe02cce157cc97fcce8840cf251b322d7c536f719f1be729978f3036b221afe133e912cf48a64c0b1deb473eac0f28f3bc8869acc99906c3b2d66e5a6b7294399e8ab69ef649fbe7bf8b84f9ca8afd80644ef6753679394f60225ad8a9cc15fe92780ef1085c7b7bb1f4f0aa3af3cf33503ae8f9c4bef0f304c046ccd3063894575f82d23a9031009067110d66bf438e1f3d2a6ab5e21fe16c2f949ede783a78fffd3b3b37df7cf3917decbc2e5a8ffdb9f8fed8f7e3aaee01abe94fc288be3802843444963865558a9e0f4214a189eca12caa04410531c57f26d8e0246321074a67567ae4cc2e504a130a1f45c82ce7a69bfe284756db29eda440f82c05bbb0854b50629d6e19856ee02df2683d4c1b693229ea5b7d50f4c28dcdb08e7d29ab609fff77406ba11b7f813ae9e342b7c0987732e08e76980c5c49efdf67eefbd2c5eb2be8630754f8361c3d02e3f19acf176f0996c65859846fe1bb823699eecd97cbef1d53812531bac25c5977cb7a7fb9d45f30153ee6c3d67177448e0f38517fca09b3eb98fa290364c7f0e4cd347fce47e2669f742644ea856f56a15e3b4b230e5d7868dba5a4431bb2369c821ef622a6c725e2b462a1ed2807aec282eaaf9212fa0a94694357cc2c448d7df4946e38e9b5457de2eaaace7a50aee213cb21ffe3c23161381c11c7d7116a4b0faca8339abd8c31926db220fbd033eea5478ea30630bb063a15ec62f823ddf01be71f1c3fef98d66c966ab2c83606e234205c7ad61f7e9152e73bf2002a0d85640854cf5387b62b670b7076ca977ca6f39f875a05b4da9747c74e7a2f4a0cc766fbab1f361a976113465b5ecef129c6f52d2f63dceb31dd9db5eeac8faff1b8d5759df5eeb0777c9d758e7f0597bf0b9fe1533abf79fa01429e92c0f3e2bd7221378b9781912e4759598282732fff71ae951bf0f3443c725279655aa7db90178e9b408babcc54130a5cb5a38cad6c08dfdff7aff7437c51693335a421ede5f2f938d2cfed5b6074d64f1fb0aafe996c913ae55755d2403bf53de07da51ad9bac3a2f492f6bb4ee8a3e46639b26f8bf4cc723bc17f56dbd33f2beb2341c1ffd65f7c93cf1e5b346d9937a2067ed9aa6e282a7be2b9f7f344a451e1affeeaaf3e6d94bde249c7671b1e6004e6f3e9f7a31ff5a935652722abe028b0252831e80a1502334cc4c1250f8e7264b6e3a59e77ab9659cf6ff5ba521d44f15de1a1c22084e2d6a422fdd0b28c9f864678f991a36ac7564c87a65dd5c04b840a559bb4a56843190975b0261c71d08f74cdd9cb56fc7305c8893f6d729202b94fcc668b7f3b7aba1ae8da89dff3edabe9fcdcf2b09a3ec67135ddbcabe9dd49ef5b5e848f9d43f3db70f208f4f16231e30fe0d42762d747f297da30907d8a2fe5f1e39a8fa6b3de1df61b75d65b537d55bdb7dcdbe8f96d7c7804d0aedba369ea33ff3531ab974d8fd0a69a7b4a17a3af039fd458154eedb33224ac68001c0525af7df0a805033305cca495768151592a433de885aaa8d3e9fd87bf00b2128963b676b25276f36a0f73ed7cd151bfeaf7a8dff9ea3bbf988bfe8670c1a161bc7cda982f663876821c9f8ca379474a44592847ca3e6b7f9d38532f6c32afbd061b435993aae3dcf8661bd45117f374339529442862e7c38bc6292ac9d3a0105b27ee88ddd179a144b931d40b7f947b1d918b258ffea7ff08a7b0eea674d2b1ba3b1bfb9b908ffd2ca8e0c0ac7de565f4cd749d7457d3dd975eadc50fb115793ddc9499aef2ebfbdc9d75e37e78c5fdc7929ec2fce5c1b81ef89b606367fdcc197854cefadb998eff3ab2c43d3067e769e5427992e18472389536e5b0ca1424e7655184f9c2a922208e326a4989afb188c229071833902c72036eb9c455d3c23cddb17df0cb6f40dadacd7daf6ccdfc57049efdd27f58be9c380e39fd62ed6739bf9155750fb7c0d807ae7b58b11e3bebdc8c3eabf522b487bdea8e89be899569d4fe666cb8cc1a1b4bbca09470455e936305cceb5df41f3d5ad931291d198ef8fe59c793c73d7d39f1ceceec7b2e07efa1e2d4783cd4da8f837a3a25f8d0be14c2d52a2dcd582314a43202111b35a205e4b895806e1572f5117f331c1975154e11a394aa56efe219329a44853694ba305a0262c870f27fc9970bc452a971fc15e67455542711bb55563c5553483dfb6124112720d10c25f8d2b21dda6f7db4ccdee8dc5b8f6bffb79bdf4d8ff11791e0eaf9c1ac5664163b8bfd0579fa88fd5fada6eba4675f7afb7109eb7527bdaf0ef7d8b26d78e01170bcba230dbfde961a612e39638e32e07c016671e12596338f68d50e593626a179ff0acc7655dd91babcf0bad7bdee3493d953a361e89e7aa3fe3bf67d7b5b742b7c81663718e86fd45e44f18d09d16d944efee526dda2286f01a55f93588153949a45a126efd2ec682f84e2dc4d97f7bbf565f5068995563957d4eb71f1c633e3463bd135b4a23edf9ffe58be94c3006923631b33e2ba227d3b904e8b6355d6ddb1d471492c3f587137c46375dc29f11c0453383042e2cc18db906c849f46b038b8d1c16c692ac72b9fc203411c69863f8a8c763d069d92c444da6e712dc34eebcc3ba710bf1bd8bf22d7f6bb94934e1f06a622275930e9fb9b863caf048eb7bc84463bdd72cbb09a1e275df0d8591fe35ecfe9eeac7b8de3170e75d6cfdd7aebfeddc09ff294fd83db3eb138b80d87bd3beb172ed48f249dc659dfbdb0ff56d4ef6fe7098984220f72dc64713f7ade724a83d02a4386e2848b4d49d04bd6c4cb0720944f8443fcb209a4b42fad82b223c1d804d2f119a4093125342e3b5bf3421a54452ba1c16ad1d136814ea75fc2421cabeaab6f951fb0aafe1956d56fe46f7355bd512a72d3c997a591de4208c662721dea9b0d18ec7bf55f58643f703536dd4e79eda127399b719b6428917731dda3f7718ffdf4961e15fa8dc4516527c1defad6b79e4615b78efa4983f44897f1b6f73723e84f8fb02bb8790459621451d268025fc419a63766a241c6e43982dd9505814c129cb8c7c9802b62d28377ad58969882db154c5850abc464668dac8e9374ef7bd553512d34d09619aa56cffb99bcfd914aba609c16b8131660cdde87421162377766b37f1d84cb38f9d9aff1cfa7ac57a9c59fbe9a6e5977ce7538d771b7b9071a81eea4076fb97c672caa3c8e30e814c041e542d862f603c3aa3ade3a21ce3af752592509a47db271f3c5d2b57682b83df511b8edb6dbee50471cf3ac14a1d271b6a29bea4f1810f4a4c455d3883d2582807cd2edcbffaa4a397b9467058ce2d8116956a3a15137e76a6fb327bd4dc9e5e6c1e6674ca947dca10924f0eb832d0cfb5f98fdda3cd88bae9115751ccb6772e5d9be185beef8720d59893361ce81275822efeaaf8d2fb970260b1860c853c63655acdff8152cd2f106d2465c1f19193ee5feab793f3ad8d1c51083963a491d69fa2ba3f5e334384df93198de39e9d582095d68b2645f7c5971f4cdf472c7c11812a42bb07072c9fd4debabe9ab2fbda0ffc36aba4efa64b25a4d97026319bbacb3de483e6ea2eeac8fe3befaea9761eebb6f77317bca530e7c1a79db6d8b03dfbdf2c8be756d2ccbebbce3f80604e435ca97abdf659b9505790b9f953d05c3ff66b0490a16c4a1535db21b1ae6ad8bfc64ab8bb203b212e30281cf78e2c88606708828c6de1c665f789eba2bf7b61da4c4d1101bb531e5926464567a66823cf94eeefd0eadaa4f6efccc84ddad994f3657d5f9a4239f519c3e0f0250a8fe98f6c7bad22ffa137dca15943eda09bba1ec6b4bbd36d3dd7f299da5dad4df9331b475a7933c887cf3aab037cf0f66eb0bbecb73b1c5b73f92db5fbcf66d386904667c9211a928838e60a0040a17a9aaa5b544680b52504b14e3d4331db92ba8e9a8a3829e99d968552f642992acbe7228373c75364125f47990b16503291269c05ad0258d7e25edde761329a69190e464de1e05d1a9449ca88070e88780785eb7f5827b2f4f19fe508c1e0eada6b7fd8dc0db6afae11f36aad5f4c35f79e934b7f1831f81bea26e4df8ffbb4ef8593509af99066274918b8a6f3f38b8f897c5cd1c52893567fd465f2ec5591f87b193bebd991a8f4ca5a7d3833f9514134e2615758c1928731bfcc87a91fa1b0038d1c1d23bcfea19a704b5ce7f2039acd3d3a10d3d1dbad0b05a1c48eb38e55bbb26692bc5f9141798fa8e7cdcb3ba23a38bb3d9fef4d2245f01b1f10b2ea91370dc460e59dd585709df516f099db6eec0f5b2ab29c651fa3b58376e3d18fff0a5f54efb168e98676c33b89c48f8272c91b651a32c541e04ccc972cf6538816bddc512417ac52fe9f5f1cf9354f0a55ef44de376749ac88a37737e4e2f7d68ddf085d7e088074c7a9d06bcfc3fb1cd1f0a494ee59eef4c96a39fbc07bfb6bc50ce3ece7df3f9cacb115b5e3a1d63f6a6875c77cc7b3cc6793ca5bb63deaf797375fdbefbee5b3c05677d36f3602b0c37b7ce757d65dd7a3bb39d7f88dcbd3efc839ffcab8f39223702e0b737e172593c652edb501502e1aab1294fa6b005f913d1e0e221384a51d149adc8a7a945f0ab9e35229fb6a44e286b524b07c5c66a28cb0100a782453c417ae172b9733b750753e2aa7abd557a7855ddcf352273cfa7cf6742401afcc51e916e821df98fd74e6f73f3117b250241fd305621b986dc78b76be6c6647db6ca0675bdf53266e7c73fded6b6be3c515a1ba1df886d808fcece262fc8b09daa9b8fa3911e1e549bb20dc78c403ec9b85c7e798c22a2a11244a011fb04a4ad1e1d155c6629de4ea04e061178109bac275677a4d4436847d8442cea5d58eb2eb894252d443a256bce15181b5468ab3f99486c56e296d53f64552cf15434cad46cff8d454d26c9a2a9a212f2721bb158820235339bfe1ae713c2f0a4355b603ae2e117487b894ec1ead747bbf3d757d657580f3ff5c10f7e706349f0e1d3bcda28f471e3d1e33b2206304e7194f92d02d04df7e4bb5ce5d9086bceba3f8674139f6cdcaeaa6f8cd2315986f68ed2651018fbbe4a1a74f2a86cf42c37c100e3705b889265d2512fc1e33f79f536865a1865818793a942be3974c0722346acbe468d313865b7b4011eceee21c44b84fb1f595f6f5de9addd491816d35bfe93ebead316d4273ae957ab03f7ea57bffa69bc03f4ed8e5c99bcdc2ac5e9f1aa74a63356963bae5112c7bf1219716db3b656279f3ae11989cedbe0ca18f11c7b4eea9e5b156a71a3ead58b81b62a4e7a24118edeb6d453485bc052d600f688bc7d0dd893ec9c2c3f8563ff8b62ede02ec563e2c4a6e181a1d40bab77f1597cc279a9e5ad73d49697d537d36b72e9bc95cfd6d986fa6c6377d27becb8b8154667dd7439eb339cf54973d62f0c3c99cf0f5e012f7fbd04451d6f72105980c90850dc65f89c27e680b2ae0c9a3aaddc1a06398a6c365cca5da11623ef6188a5bc98cf9953268582092a6954be1a86b21502298c6db1974d52ad42b997b9f82e68ef7820ebd9abcecd7de484393f2f96f655f5549a1c64353dfad47c1fdb49539c6cdd7cf4d25c00c25a5a7db517ed8980a394ca45fc0c0b0e6d397db273303f20ed3a433d1e3c3d393fe103a4d5b70bfe8ce964f2f1233cf5313f8bec89e7e7d003f46dfaec13b11e46a1f4b7e1981140585e9e4f232a2008864eb686d8c74931c29c1426cdbe6e0f021b81895ca96c512471ac46b9280aa09134cd13a41773584b3611528bdca718a408a844c839cbf36f99a57d12e8746dcc3b6583c29c906ed10ffe42119c90acd2c26bd766e3e92f65fe30421493b6ca28140dfabab6ed657d35fdf8cf31b6e626ee4d377ddc0ba4e355e15ee74ac57c56eb8bae14adab998e373bcf7ad6b3eec331f88006b1f6c0c2fd2637c21296b3e72f2e5d2ac389bf3eacac0fcf0ef90a8c3f86d4de2ced2f966e57d58fe7fef460fa3959ad42c9ba26e6e6baa9a33ca8b1af38b64100e56aa8684eaa9646ef48a8e756739af4f03fe5397bd36ea546c3767340232b63d0945e7baa96cafe2ae9cee4933a7583e7c0e362a7348f1e0efde0d12dd7de8a3a76e6158ceb5e6c9bce8c368eb1ca0867e0bcdab28dd9e78b81cb973962248b1fd9c12e0fda5fc6471e48481adc9d1503384b5feaceaef2453bda789e1f8f130e6d6522bc0c36a9a14fcc29a68327fd14b47e431bb864d35ef5ff9f221fe7e2a0037523c2e60ba462f3427f584dffe2b4fbc97483650692a32d2fb774b817b60dc78cc0e6ea7a47cbbef573e732ce388eacac1fe5ac9fbe389befbc0cebfc6eeb45e795a9b09bcd534d80ea07074528ea9aeee0289fa68962ce01c63f884c96dc5aa6b4ac6e284d2b8fd4d34f4096c8a55e9ee014e134a6cc2ac778fcc4e0922faaa65b3d857132fd1a64874f443f70d06187ee0b531f1ae980b124e9541779b7eada476f6c87b69b1e18d50084405d0bc8e2dbabd200d33de897fbeba447879b2fdd3cbff5d6d58f3389f56056d4b979faf2f472ba7cc6d12d3c7ca8e3b20d478c809f6404fc62795fe2a90028390844136e1d74e542675eb8421d7c854cf4aa31a4ad1f2c91cac052a680952dcc0d8018914469579bc293226f9bb52a2f097b46ec042cbd428fb02be8916be81b5773054b1e3a0a761c7de34c19f6ce5057e2c45eaf58ad26211af930081fea0f57c53e2ef842692f3b69357dfc02e923b99adefaf2677a9faed778ec444f0f96efae272df054f9088765b8420c4c43389bfe50b9e8bcccbb3e284ee53170e778b1d455758bfb8ba5a6c76d99df860cede7ab5bb9197740a25f0c7efe4b8765454d98167218123b41a97f5693480a38954d1934b4085431e9ba110872e770eac45eb4d4b09860add9f26ebf08a253d7979f6c294b4d262e335ced2beaaea623f7dfe1686ac51ce98c92431b03d801f0c5c2d8523048d72248d71b6d3b70eb10cb07e9d4a26426047258d1542d3b5badb52aa901a49490bad1bbe45df9f431be3c8c0c0ccd543b36a4acc4dee722caee2b49147d98af92fd8acbe81adbfde6a2d3f7e1fe8bba6c3fc4496fccedabe9bcd63f7c33bd3be99018c2ee6e3ec7387135bdafa49b1e10b6896104ba6367dc0f0bf9d42dfbd6ef5b7467ddb8b6c10c2bebe7d8cef2d7b8a17e7ff11e59720677a559fe2a2ffc6c69a2263b2579f8044ce4ca446ecaa905a60256f524233eb215990cdc3405d621b2be75ea9c5623deb5a8d85a693a31abb72ea8ea5614da8e81430b6c87e5eaf96cfe12fafa80abeac19e4cbe2c371464d4caae4b753d768c1ea90b5e7a2a9800abe5fb4285fdac5e4ac871a06f5467b0c726ad35d92d1b8f08d79f129603fff18646e46aba39f9b8821e9daa7de9cbdbf59fe8ff9f3e1aebe1436b981f3e9deb8e028f74be1f63c72a4c8951ad7cb019046171f144e550567d7b5b7d50f088b4b7b5b882a069e887974f953bcafa4b1eb1b7e0ab071e091a6bff2464b00e517d75c04600f86f4c706b4c82ca174c15bbe8b54575d400bca051c024649fa459f8d252d80d0dae8244e86daf9aec6d5a8f1f59f9f5a03b3310d656d3972bc77c872fbd584e7bcc01ab559ba356d3c5ebdb357a2cec4a878f7dec634f40919f7ca5e95e6df4d69e48ecccfe7d13a2e273096b6e36e57bddec4d5fb8585c78a6ce7adf04d363af0df615b3fdd952c276553dc370ec093df90b16669b84faa5e210baae397146b9d4b5249b19d68650164d0c739241c9a4a1ee5a5674ac968502f519a0f59207ae46e745c4dcd15b89ba20642b1d336be82c271f90c6a97d1e70993826f004aaafac1e8351e0b1337722e2a35cc86afacb198033717a1c27869ad1a017b8083ae58c9b4e81900c64eb9f4e8340eda53a52b657ac96766183a4f92cb6c479b14a393116c511a23d6d7ac65ff446a227c2f9344e3d8b01281f3a68fe69bb2b059f45d0810b7170cb4bfa596eb8626b77a2a5fbc0eb2b23363784e6728c5f201dca468961cb0b4efa5d7765fa48e9d5cadf51d71ff36477ecbaa3d73bd4b7c2f455f54d679dad47f7c1e3ef4438f3bb06b2d7918f930eff33df2b13ca823642796932902d31ca9c75c4310627326bba05652a522e4ef0a01364116271889553aa938a5c4a07249fcac579a161fd209b4f65908357e76fc617b84167dd62f4e6c8afab70ad7f066ffa3669944e48db0ea93726b56725e3e99e79713dab8b85346ceb895f264290a0ca7f9bac001e0eaee81f86b2fde508e0261f8f40994cd997ce28b5019b3e6313a7d378a078b3de66bec67913bacd33f4b3ef414433129e75787598558c32cc01e40e4e635e4141a2261514e9c81669654b884226ae8ced4ad80803098a58114885d2764a351a9be8848299c7d9a0d5446d4542fac07454b387540a44dc7e08311dc904042c7da28e94bd4ebb68d54c105116011dd1f26400cc7e7fb59a9ef9015805de431bbe9b7ef1987b5157133abe715f4d37dd57d34d3f12819b84afc480fda74782f6d546b3dff0202fef28feaecec5d89281ba09e4ddb283d9f7e51a58523f3bf6d2470baefe10d24d37d52f966e57d58fe6f81bdff8c6a761033e5fad8a739d1957dd41739aeeab4a429aaa9250a70b50fb922daf3a352b5b3746a4349522543404f4d5b278802667826bd5a2d0a094f23727906ca870e286f503e5d889d4821e7b5b78e2e66c4d4f3bcaf84749efbaeb2ecde21074e686cc5590a03fb760af7f28e3cd357bed9a498f8c9f8e08d01ada58f8c0f334526c9962a5d843ed395c9157215045dafaa432eee8983852edcc0ddc560a2d6473b324a4719976ec491c17c0960ca52e11d25ec8788e730e3e31becdef62cf7e47d2592281639b5b5eb0e7594d97ad3ae9aea6f37413f61e5e4d5f7d331de4e6a48f1df4ab8dbf5ef7d51874d875ccc62beb2739ebbb17b2bafe0164eba570f66c39e6ca84c2d09d6193ea3132a070101221808129200238229e29a4ac90004a47cc009297461094b1c0955b8ac48bfc264551c99d14e228833dbe11880e4c279f8513fef54572e5ac2368b1287daf3a7efc57d425d861e9e083904c3fd3b65df39ae85308ab77d5bb38f3d1855c71d54f715d675debb2bd483d9ab8d636f3a5daa1d3139f7878a37abff13a843c02ec2ce72fb07719d9e5f4768bc64ef95816ba4c1c158f481e999443dbb031026c7bf96a64e3f67ce12552df051647187bae88d50c057b62dfc14e8c2a90b49c88a0042a7d951328b908bb09cac5ef86bfbff4d1d1a5408ba07120b435f19b75438ab4ad2c150e625353bf410abacdcadcc8ba08d4701289568849fbd9cb0ea80c8338b4015cf4b4958bf1da2ba46c397d5fcb9e18edecd4448fa20ebf42fa58aea6db59aeee8759cdf8ff4eecf87554e88dcfb97317df1939c900c84965a019161e67f61bb3c97cf662561f9fe48ba5f1d3398dde311db6c04ceeabcdeadb55f5a305e5d46cf6e50e71ddcc3b35aaa06ddcbb22a15731bc2a5a2fa692689a9168a0b8ae9eda8cba1f45b610488099368b9d05701eab621dc1c040c41e7833963e49cb84b66731fb48b29c86e7c4248ebbc1eeb8c7c53871cdc53f0ee3d187f32324df8b353c53ce0563a80dccf5738e5d96010ca7434249be6a91f2810b192e4126744ed41797374287e1f55397615a21617bc1c96262e393f81cb22c6d244f1d7550ba16b42838362d3075a89b557ff23ae632d876ec87bc5e4ef872480b3bac237260a38707243ae996e6f3b870a76f7911367e8194f4705376cb2db7f45f209dc0d3dc78f5d87adb707923d01db431f6d8597755bdafacf3fdc6fddddd0bb067fa0e8e1f84c5171aa74b3e221891386eca95036495431929d913a1e449f3a0a950e28456798a9bfc216014f4393f38caa13225be0d8b999893c422a725acfd0634cd639f1457ed957d42f65e626dd2c72e6acfa6fb7fc13eda89f4d37adc28a85bea09632281ea1f58b69aeb3436506ef5d69b80ea445d13d41d1ac79e956ddb344deb7b5faafee135f5ee7057f931e77c0fde6ba757fc28e65bdef2963bba83df65e072e3635a0838d77612c2e3b18c49f63b220948a10251425109852b428d018d81cf240806b10eb7c658e73af59076e7c44a8342a61440fcd20985d372b166f1fac5330f6ba4a70281633aed5966de7ea47fc029303bccd4e6ec97a188a74e3de90560a742428500c747ebc12d7880997040f51af26785ec4f3fd71f2ef18347c3a48092eea364c9f37809c3bf4733cb131f9b3f9aabe91ff9c8475e804178dec73ffef1ffe0a55eefa1ef1d7fce739ef34938f77e8527db219aa15300e4baa2a00c20417b14bd949707b16ab358b6b31b1bd61db3ed5ef59325077feacb75a8d4273fa556e34cc420c7983bdec02d71b28de6068f3c7126af2865d5a915d4b22da5f62140210180063c9a0e382f9887973aa414ebe8d321e1c5632bd972ea7c20094e83129bda9cd33ad203c457a33387bdfb811a61c7ac2ec0b12f435a912b2ff2c9f15a6033b577e19500e16decaceff8839c8adae2d863612e098602671942c1d2f980a4f0f83a64ec8b0b3d79a789121d14d3b6aef35f81d8fa90145e0e1929eadbf77cc9238497ff92cf6b7cc83a3bf14c065fbbc8b4b37bd3fd421d73c8be1fbea8d5f4d55644d651c2febee5a557eb44e0eb4394884ee1f11b8f1db4eebc7567dd51e9cebae9fd7d9df5dd4bf0f85761fa4f0c3215c56d72202252e17f9d901b755cd141c89427654dc90b8a72c2d1c5537be08da485b1fe0895322c20b22d3aede54f38a89288688290ad73d0a82db7d2012676484a63793b3ece7f69159d75aee5d01760e8ec9f8b8d138976bdbcf27bd2112169333704ca3b58d506d7962a285083e74ad59f7412af2bfa14ddd98b87ae640fab10d527494cf8f2cbe1502beae31f3dea3c3b8cbb82603b9e97a1b20fd591cfb3b43bf95d06c6b05e26ac97f758d85121d77e54c1e31596cff72d277fc5312fe1258160283e9e75c12ba598b01546443855e2e5a88145c5f02cb2170e822cc136aaceac2084c13afc69a80a85e5d7f36ca93a109a692404ea8e32c24b4914cbc9c3ff16e74601fa36a3d296f2156ef51eb6db9065d621db57f5eda78e7f6985182202f3ef60c16a749f107a2c1dc24821fa4ba49babe9f5ddf4fd0356d6fdd536bbf7a8ec4db79dd97cfae34cb26fef5b42845dcf61bc4f1d89fcbd08649c90309e4beff28824212b59759d4c5f7afaf4854ccce7cfcf76472bea0ed56a557dbb57fd58d1c154b03fdd312ed31afd89fd008a5ea9d2ea12833e3888c953c35a310d56952744de703b5d690b2aaf153270360130b705920c9c536642f45f9b63b61da6d57fe6da0bd89cbbd5e09593de32a3cd2bfc243337daeb93dac6d719259970b53974affca9577e1d037cbb4394f50f87abd9d98c20e90c9f3ac130051607000e94f7125e65c48228067f6d905dd994577225f65b27a8ffc9e7c6b31a9d3410e73b4e157886484816546c1300877a18f727b2029edd93f7c9171dfac72df4ec7f5b2d1d4aedf06a7affca8b0a3d63db8b9f92d626f7d5749df4f16aba54b6df4c7714ae5c38ca01d359f725535b5977d6f7e3ac2358afe799c7eb4b1e4412539951ae90dbc84a93093311a794948c5121a22e6ef38acb0e952c49cd106738e2afe0f10f6eafe7f6aff83dd0aef921596b89e8b9cc8c8d93d779b707fc38d7b786b6abeaf54544b309c8313f1c397d66ae4402a1e5f5500ffd0928506956b951b935d612cc1673fa19179e760b0adc2aa8516eb445c4b05dd22759337016781d7b47ee53afd2f5f3d8a95e2f994c7ee337fef573b991e6fd177ba96db0378b3bc4eb7c3fa9be651e1db7c7d6df0c11814de0e339bfb777ea5b99df580e2ec18c911c0f8870f8225cb1899e2429a348c82b807ec9a58b5119598b10466db2f804195c73a944532d2c0fbf4560e210b556e2aaad3854107075a5b7ed8411499522954b88032cba70b9bae5a4cf1f750bc2ad46ebb32d098d934ec25ce10a149ddc6cf647b59aae8d191e30915ef3d393a78d1357d383d44e8ff4de74bf87cfcad5d7c0b37f336ef77a4ef71575af1116bfabf35056ca5bd9ae01d68e7b64a5653abd6db13fff465f8bcfc3c1e6acaf6f59cf970ce6379ddfee55df949fb7beee75a751bfe766ccd55dc6b57453bd74bcd5ba96900f71ee8853d87028062921b82ab9000865a5cc32f1b5dc16859b44d256f72d37cdec56b6402da66e3716569a2d3fc0125eed6ba66cb8c726d1974f5773ecea31f1eed9b3fbe3afa87f217571d0afceed118b095f326274bc78c6cb28cbd9c666386a7ce410639f7116eea07a80c0bff631b5337ebd484748ee148e512d703407a6d1aad570c96891d5b5de4e18574db022d8df39aa3dc8cc1adada8e2b43a1572f8e522fcec9ec9ff3433a9f8ca7b76e8657bca44b32537ebae5e54256d3572be916f7d057d3ef197de5c5b2abede6abf7f75a8bbb0366dc0fafe124679da7d27762933facf445029507d88fc9405691bd411e85235bcaa065224520cb442409281298328ac55794b417c95ab1aac55e615bbaef223c924e7b3af29a1deb34c5aa1b012ad517eb94d3c973e7f3657e1d747a9155f5dddd61559d6bfa86502b0290a6577ae1b942af435d9006695b4d7f699346ab7babb3cb90e6404aed2c828817855fb769a23d70387aeb8bfc3aae2eefa5fe5796d957472cfd99cc9e29ec2407ddf271d874d6c7653dedb86fc3da084cbfaff1ba0d7c09528447e508549887020a24c6d4b4125bc0266a2804001f3f2b78ed318de8568c0137866e0325ae2622aa99374a00c42025aec25dc01e414341f1312e6852e1df474a59a1495c029d8a2a28f8b94ba0fd5c952b39e95811f611ad05d195900be1f7d78afa0e735e7d9337adb1ede5e060bced45e87a38e925d2477a959bbbfcbfe338f02cee2debbdba7e737d4ceb266896174a65a66ccf8d1cccd519189c3b790dd3817d377bd55971c041e3df957537adebac73ac56d5eb0330c3008e6f0c06e0e32cf1e9db6e7e2e8fa2f952545d7856a248ae261ee1a5efc1d09e44ff89b8632fbea87632837fcbf8cba41ca3848e3a6b01cb7fcad55b40c87726eb6a21bce514bc42488b99c840e785f05518169d48382bb1ca171fb0d6d2572beaaea62f16371f201f073ca65ff44f334ae96a72eae8cb3319d717394675edda417b89fb518315f8d2b7ec710e0ca28aa49d36e7d897090c07622a07be8a4b28c722a4200dd7a81305838eed99f524ffa4223d59527d910065b1bbc0c9c6ac5b68fd120092e500d8b3c25ade4b3b6fea4b25fde5519cfb818d3838ed174877f9a40fbf40cac6e0538be35f20755f3ac487174893de9eaee80874876fd3813bc159ff349fddfc41e4e2422450f9d578eb617b446294153022334a1081b44e7664d1e28246cc457385bddf400637b2099282d99ccd1579e558225826eb8ac6a1347735aa7aca310748e0ef2d0f6a55dd15f5bdf68ba05413e7ab8a40c93435da2571855c5b34042548fb94e5468206bd71f5aaec61f4d704fd725cac557d221f5cca0e98be5874a805886119828293c2837f99942efc252956fb03ed3f3de6b15b698e3b861a2da18c8ceb8ecbcb4a8d218fe374565e27d32f2989900165b815807ea716a89ce9010135ab188d85c99572655fe36b7debd59616327dd4e1b48f50c16af495400228a614d8403ca930148414294b5226401a6414dbba0b251d418ee204d7d5a45c071d1b1c08eaa85c52cd63f2288be8d62382766fd1d6a09d1749338b83d6c358158edbf622ae13fce6b697477a35fdde7b3ff202dea7fb4646f8de273ffde9bfd7fb7c54dcfbd2e3a370ae2558779ef7f6f678a1747941b9f36631a204679599c882ce80f2e36936fd7cf8fc95fb3bfbf3fdfd9d3c2274abba5f82196f59af2fc0d4af958ec7e47a19bbf1355d767a31e3a7a4d113959eb156df19d136dee87af4cb32e06d828c5ec18bd2379c318ac31a9415f45456439d78b52f78f40285c4be78d2ced88eb0b0b0d300a20d11ae8d49b07f4b9e8cb1121b3d6ecaab733ec5af2ba4d59957c1fb8ad2a5f16afa0aa39c749ce38e372e7a4cd2dccbfc8003e1a45e5fc172201914f9e160552e83e68bf9f9d20ee565a729f7a6290eb203499a021d820cb4644029001240edbf2a5285b6002b33d83d1984e0cae1aa6459b1b4394b2235622d4adf04e7a68bb88c36715b56c471ff79562ccfd587705d4edff81c63e3a7bf40ea6d17babcdf9f8ef42d2f966c6e79d97e33dd5179e44377c68cbbf37e94b36e4fe0d16f13fdb3f816ea2df28024458633cf2b7a02225e25df2bd6810000400049444154876db18582481a85c41a929816e40e39ad85c122e68f1a96ff005c6234529f2d556e0b474aa51b14fb074e6c4a75a69a20ad9e04633afd5afa7eca15750f6eeac9ce9fcafcf2fc681264256d5bfe496b86c0e73abdc856167d6b97d11d74f185d35b2262b2b171f65da5c9e52eebc1e0216b469b0f221ce7344bc2325a8bbdb7c3f6a73a3ef9bcde840efafdf7df7fec31de0fdfdbeaf2d169f4d861d8863e028bc94b22000ab54c87f9253c32c13cff0a524952ce9d3f117e95803a91b5546112300629932c748d039316137ab158980a605d69d83ebaa5e05bdf3996f4ea2e597808577b22b96a6e3b563608228ad25942a7425b1cdb21aed57ed7904c838f479b90bb0b9b552daa1e117b5af7f322e9ceb04fbdd037cf28ec11db5e3e39a08d5f221defa51e10ae5042a771b998ff42866439f9b5a3c876c7b23bb5e358fc5e7e54ddab1dd6c7f6733ff77319fcd9874b32900a13ca8302128fa0e5298843329dbc647c6dec40dc7545bd3e07335a556f48e34f35f636c7f51f2fe9e57cfa65ea7edf4ea6718d9a365dac6d6bce24fecb07f5521cb5cc10c644f74d6b7b448b5626691dfebb5d68f8a1273fa50a4a662b6b81ebcd82fd88fdb10c04c02c86b3168b9f776a34998d3d6db633d7f627e221ac54780099d049e7a8c971ade4d1cfd00fb71fbdb45aeee3eab838801900079034a3d56e7a1c9d1ab7560b9d087f1c3971a8ea183bbeb1ff21955cc6d65aa21459712a6d9c762063bdca72a6b1ea8abc306f7dfe24608674e6112681aca85b1e9cd9fba60707ffc61faa6a6f909ae0a8a08fbebf3fddef7bd369737f0f2f7d91d5f4d5b617f7a65ba36f79d149df7e33bdc6f0d1381fe58c6d3aeb2c6ae53e9a27d73f09ebdfa7b41932bf234bd1e2c8073015bccdd6fe3852e4a7d9f5ccf10a5e244c7124e397a00411d897c299435a4595f24863700a0feb413de7862ea259e01be8f6f2c2a68da7f262f60bd240ee122f506ff975ea427c0f9ab3059bf5946b533f4d0956375359584fa54094c07cb5b4b96856e0300fd5d049658c5bee5a77588468b7af0d4ec44bd50f68affa8dd4aad62ac5e71cbf82313993bdf5f429ddca6972bb58dd495fd5389cd2891f3beb62e8b01f251f61f161128f3f880e19bcc641519810bcc80702a351cd6369c6a46426516423524b8d0887851e702b825646388a2103a9903da9c4bac601b593ba53a46dabfb4eee71af2079053b145b1fd28cd02e98a5ad9998eb1a3c5323c421daebb7369d886a0e92b2ab7d1678b4c8c9a5e1b49b904fada68442f3cc76927dbe3315c38f51c1b838c5af87cd6d2feba5ab174a37e10f37ffe4273ef1155cd1b3735df3c95b3bbdee7c8f9df2ee601e17f7bad752dcafcf3effffecbd0bb0ad4955e7b91fe7de5b758bc2a2808062264a83089a28651c629056c1b185116854505e4548cb48ebf868ed860ec656bb277aa27c75c88476a03dd334e3c8682bb66223b6146023160eda763b2861dba18430485509f5a0802a146e71ef3967eff9fdfe2bf3dbdfde679f5b553ceabe4e9eb3bf7cad5c99b972ad952bf3cbeffb5094efcb7867fca5088184bd140f5448336ffa1494db3596e30ec9dce709730466bca54edebdf7fab5d287ccc7af6ab48c34d4bfd4dc7cc917f714b510166a2a534e7ae80965597551935d4cf0d209c898991a66b539201c618bab644421ca269f75c44d9ca49baf3382cb623c75331167e54d16c73bb2146f06271ae1e38be9fe6deec4f278e1dac78e7c8ddf78f65a6c7eece8902d758d748df534e21c5fe6d3e937d3c7ab4b5baa31fdf927ada03e74cc1ff4882f9d8b7c12bc4819e29b589a5a1d1b2ca1b3058c6568482fdd6ac10cbd65cdc4654c6508269654ef2ebde34c5ea10a37b4f16f2d4ed9c29e66d0ce3e87c00fafd62edf89228e1d278fecf6dbfc7b18e6b9df9fed746e8f70e4c576dcd703a4dd48770c1d4bcb18d63f729f1f0a74634cbf1b84db8c75df04339bcdff3e3c705a3e58b197ba41dee23f899db7dc74c37e89cd528c18de8f8e29be2b9b41c31bc79d1ae56078189422e1770c20f9b478cf7ad5499428d624afe1a6eec0880b985a6c5a70f24295c91977d57d03cc74faacd441dbca26a9768a25fda24016a7a221b15e954ac4d60df5b6ca935c6145abebd892ab3c6eda4c95d12e8465b6385e6a71c088ef607d97bbc737fca749a1b28daa0f59844c978fc2f83ea911dee11ff6b087cd377f3d6f6cac8f8df4ce131d6e40d6132e551f423e07557bb51ce6afe640192fb124abb6a38c49335d06f7aa9c1894012d576545e3045dc2247c14b6a0329e70849d0cb09283a9048e241122686e6c0b05d66680938030a43283a4cbe4eebc6741216c4b5788ab5dc23b41999746341ca4d18ed461e31a27442083b9f58eac12bee5ddb646377e2d633b085619a32b2bd628fad5bbd31f72e0d88be0dd681e15fd9c04efb8e38e273015ffa33e36cbbde54d1df161c6f87da5f7f2178abf6630cf267f5ec79c1ce8c6b3f8fe87696093b2274c8057a6d317f12efc1c7dd158bf8cf3ea39a7ce85ffd559f5468cf1aeba499faf716dd59d77de8d37def85864e951102e06b4afe753f274f2a052accbb31f4e824443e93028f9ca6d0112b040958e4e090e7589322c5ef3f41d4bc2962554488c5a3eff1953f3d43935112edb03e1166020cbabab132b210cbbb202c77986473beae38f1d9d4f461d3cfcf7a2c3a09fbaad26526845589ae9d4854eeed2d1bf0aad6858ba52c0a2a157c7c1d2d185254219a62a450e63c7570944df0aa93f358034a04872c166bd8066971c30d3f34b99325cc457bba0f8c278e4857f4affd6723eff53416b177db46da23dc2403a963bcb1d62ab232feea65b62d38d77d3cdeb63a8cf6fbc5edb2c7a14ff1c51a01beb63741ffde8478b494964b3c4e741587f4dff04d679750cc1002bf9c51fd94e67d22ffe36d330bff08c3e9c030fba6e971fcb0658f1642f58c6b68c0a1c4c1933036c095b16fe967f0952c4ba9509f1945e4a79cad90e797ab69c7df97c7ffe28d78ccbe571ecaae997da3acbeb7c9564ddd1aa9df04af46a6578e848eba868c9a8889555afeac1b24be868c0486bf507d7ba66aba40778dd3496d78a4fa74f5730a93daee8638b67ea4ffa5b4e03bd87c7fe61e9c26ce38b5ecf18c7a51afebb2a5b794446d3a8f12f8c575c5e79c53ba151674a2388043ab81839384c7372144bf15260c25c14ecab472ba35a80ab6c3833a34239d233d1e09b44d5431c3b3ef8d240d26b451cd606059000046d2686126cdb983e714db8d7c98ce6ee9cf01150c2a9cd84d49ac0a78cb593ad15dcb8d6f9f413d9c1d9c8da1a1deff86e05f82c12a1f96ba1f50985972ebcf7318f79cc2ddd78ecf53e50ffb368ce392bdafb0c1fbd5f46934fe4a58c35e128182251c8e464a4c95fee2f5fe4a340dd58cf79f599ef572f4bbd7768f3ac7aaf6f6d91d0812f627f6fb9f715ca76248749a68b4d8c421349c8b967694dbca4b3e8be92fd64b4b225a365d95328282d6cb0f9d6d7705b837a46eb343aa640002eb976e1e0e802fede7e724dfb6eb5e7b43b7cecc8bbd5f56a460223377e35e3178fd2cf97e00ffff00f3f89ee3e99ae87bedec12c62377a190d21d1d5d0297ad794a6f78d1b2609a73627e860055bd1318554be291784950fedeb9984144e59696f69db907a9947aa4ecb553dc144b816d1a60939f289316ea7e9cbcff471eb3a18deca1be8b44990dfbde58e46bace874877596c952ee67c70364d3cf242784f23bde0787ef416970ee5ba81de8df69e7ee47ffe28d08db26e14e63deb0ff928af049f658cbab1ced8fd14ccf3c7f24bf1877ee96bcdc53c202a9fca4b611f21eb172e2ce68e015c184a77f4c12f9d6409ffa8a1e12198fa2ade17aba61576aa26c2257a8672b195a8978fff203bcf75e5cf7320cf068a87ec6d58158c3ca8b02c2ab8f559b3a2512b6c13036e5a6434f2d47150507cd6ab67597c0bd4f612e118eb2b0d67ca037187eda8ffdeeffdde9574ea491e49907eb4847e6b68f14f5bfbdde8b1318e3ccec73fdbd1f33777d5cdebfc6058671d97bcbbebae9baf61bcbf567e93f0d09ff197bde59ab01463505c9034296656bcb00803e4cab065a424253a8e305e95e94c6de98ecbdb3cf94a6810922a1e953be9c2e4e80d616520159326575ab3700ea28cdccb058a74f36562a79c2c0c8864fa49db4d1312879ff276de022094efc497f4004d3ec50bddfa1c51295cd7ee9d73ec05ba0d93c000340a6c9e4f1f657dce8277de79fbf7d391a71479d2f79f1379371ec77e372c37fd0e6fbaf0dbf28539df5def2b23f75f62b838b80cb1fcecf83adcb2931739ca3c196d365f3e86b1fc2a7372fcc540ded4776ad23e8474b4ab2e4d9a9b2fa65f5596b209cadd6ad12391f36112150b34f61ae153abc739208e088ea4486e868214853be3d2cc6cf58cf811dec03b99895f54d1094293a3216a519cea2747600022f89ff35ec6b6673ede51efdba81e5e5bbdeb252826c74f1ddfbb6ab4a3ce21f7c175036f483847019e017845484a274303151ce13ceb439ba458d2251cbfbc1a0ee2e448626f33e9ca843b84fa29a55f91f2841153cbef63941171126806468c9b0c0c398c1b399453fe52d02654d84d1532f34b923add76d97c19c0d7ca2e6fdbc926ba9704f0717d00077bc4ddf4dd7636ddf7e06f77bee9c537f798db0df3ee6f2f7194faf9a2c00163fdae2b161f7dc8430663dd7a31fe783e6cf2f7e119bf8150bc14dd007f373d627a784bfe83efe49fe253e2612e3547b407654c2bf5127c447c09927cdfc426b8e4d1b21ad05be46541809fbad437d9102c9ed75610baf332d8be9197f61fa3d0f35b8b65682bc9cf36a91257fd2999aa77bfb7ce51971de90b80dc95028588142db3add34aad371f8d5bd607fbcc590948c50ebb6a446fe66d1acb3d7feff4e9afa1de13454bda91a6da101b647c797537c2bb71decb76bfd7d7e17a7af73717098d1a3dfbd2f4f7ce1c7b298c74c2235f0e7a9434348f816c9a135c663b75b06c4b1203a2ef84e9f8b86365d994ef5760935fc9c9b59cceab536e160479b843a6a7b4b032a1d7c0cabc56603aaee50b17b4084aafb9aceb0188ba2d04542b6b5d4e18a9246d3383baf0ec8dc254cd93068045f00071d25f4effbade3420fe72cb9d659d89245ae7d3f3e448cf8e7ff9e5a76312c098874e1a6b053ecb08475e9e4ee37fb8fa927e9c6677f8751dedd8e0eee19e37f6cde3e3579775237dec8fe1cef770efe3fe74fae75974c9cb8cbd431bfe0bc3154f343629164bfae2056e84f4e70eb2abbef5ddeaf5aec64bf9ac3a54fd7adfb8a04c2a6fbe4d242e7444ae94f5643ad9b52ca19549a3f8792a0578a7d24a649cba0c46488967e7a6a4356096a33ecb04d4a2c282c7f12c83becab153759ab72be4cd4d39e8dc8c3c8c80c8a8dbae861df1acc9c6e63a56fae2a1ab573302725e1978189957f1ba45f855feb6756c49380efc256a52625e49014839807075d3c2dcec944b4f29572e6fc9a04474ac346de351c645d3ef8df0e20dbdad17bc8997b5cd40c01b415badf16af5226ec51dacd57891e81f5c8361b6f8f935e39c62e8dedd3c2b8781ce0b1887dd74de8eb7e7d9f4fe00e9b6ddf4fe3ac68de34b7d9d561d3fba3ea8143868acdfb5e0f99f18ebc864e64ee6d83f855d7e4c9e8de16a0be1214d0f59cb9f521f3d1f8d90eca4aa99e025f2e031f82c0f5e5a185c5558deabf2a533dac66398d60af91ff857a9c2b128cde60f0a2d37af60e8d49fab00f36bb1615e46a5d785d79b5e32cc7feab57af55784a1f33cf1c82d7ec1254120fe2ca1231ca4f82957a991e90a7ed6d74d63b923dc9f4dfe87b4d77e4499173dbada982ea6570adb8df15e6ed31fe78fcfb40bb7b948e8bddec47149c521f3cb6214cb89610e2e043c5f2e53cb1efec37049ef0c1b262362aacc228ee87a806b2744f55e98c4a2b36cc2e0526874c1eacad4f22da598592c32a2258012a631733fab6a9d994c82beea2883bcea8910583a385a6dd66dbd4c226901c5fc3475982f0975db77a1511038f33ca1ca7cbe33dacd619298b5f7a707b05dd6cfa75f95d7329ab5f935d26e448ecb7e36615eaff985b4f457a1dd89f48fdeb1b3f9268ebd7cb4e3ed06778f9fcdefbbffbd4cf7cf56e67ccbeb6d7efce31fff5186fb4e158b7c1b3e94776c3044f357b71fc371593442bdaf2507a5e311987a55e3c4c3ea3a3f5b7ab2ceaad7f197831f4012ec733dc6e23cdfdc8d6f7ad39360b46b233a364e5ae229b53952166624866f5a2c33228a7ff44ef8d482ceb82b7d22e820ef1603c20931d2699ee0a495d188aeaa192fe8ad0710c2405357f4d36cf6179c0b75670e872c23bf7d2376773a04eb71708679fc6ac6f1f9f42abfba62249f73230f63e63ba1dcc99057a2a027cb85db4d285ae52aed0522559a05141a3926e65b368323ada523e3d8d0e5bde7a0b474ec84b250822b3a3a03efb80b6072e9f01a0f3069b09b41e13cc09f60210fbe0af6a2227907386e13d356e762abefaa13ac85566d987423dd721e79d1efee6837bd53e2fcf1378df5bbee3a68ac33beaf81777f3f7a415b40fe81e73498b3c0f72a0f71191695ec000a6f5a8c5b016468799415a569ceff26abb99c232209491039ff294f81d45335c9c718a5222954c2e884d7439e9853be3368ea5275021d481b8ef35a710b6a43d11ed2627453a545cb061250c8400b2054dd0948961d3aab1b192f67854be6a6b1dc4b60163d33ade0121ad30e7dff6c1b32fec80edb7ceb1dffd6b2fbaefab637c074c0a2548f5d82fe1db7def10478f53a57980e7cad3a25048a54ba275d7620029368baca2bc5c8a40a047f94e26fe593e08e8e03280e019c30833682253ab3c29241eaac2b8098ab9c8adc988cebed205e7954f884023483d71937f5147e8a54a0c10822133961fba730584fedb8170ba4ada4791b2db7b3a860104e057d32fd2b598d557d9fdbad6470753efdf496f3e9a3fbe5037419701a91a3a4cf2ae8ee37ef61fa5594ccd5c3aa9a7ed3de9f1923de341c4f9e3c39dbfc8de1c56bbc1bbc9fcb368febf97c867b9b19c60f64e8a14b8cb75669386030fac2217e0a5afe3ac149a6e714d8e918eb3cdbc42d426f2d9e9a9cf4e5ea6771d2bad77d16b00b3e0bf9e43c3f32e50352caaa3ac1492832af0c2b7f5e22859161534dcbade43e692a651920c747a5c0583856ed4ff87aeb8bb25f5823a331dda94ffce81a6bd18a8fdc73e9d32ac57f9f5f9c6f7c19d97713dff8a2eb0f922e16c7d7ef805d55f99b578d747e9f3339dec4ff00e22f8b5e9360a15be938f57274b3e43427ba9840985e587f92ae87f1d5778976da55b982a420b8425bca6667529c24f897a0439fb83ebf564b3821112f8e7d2f5708dd38491ff43188f83bcd96eacf1fd84dcfd9f41a3d77d3eb01d2f40163fcb43a8f613cdbeb188ff33ac65b1a457267e49c2fb442904bfcb2edad1f1aebe11576d5dd59f7e1d2f974fe7258ed54782bfc1e8987d7907d991426d452097fa929d42fd131c0090f50e420c0c09394b80158cf5b4cd119e24bb67cdac37271f16dca8037462a3c5bf55987fc0c4ebde58c39442cad6dadcea84831a582c25173923bf9e2b705f85cfd8b5e2556c257ed2199ae585f41892cf1e962e3fd642978e0e2ecb539838d77b6b7eda8fff66ffff61752cd75e953350f2cb6d01ed6c6d762367fc468b77cdbe26048eb7079c55a6b61af77bc5070582e69b73cb6f73c8dedd8db8c78a422bec457719292556419c94e84f93cb74c219749c1ce6c96262d493251b21c4dc281275b264c92f80df013b6a5791ca192dbe0cbb0d6436230abc069ac8c428bc8236e8571c4150e5180509c328f758abee205597d331c20ae6dd55dd9553e61da41f5186c7fddb256de78a65fa5ae85ee99ac66f8be432dc0e7da80bbe28acbff7734c59325af7d926604dffbe8473ffa2653746323bd1be77c0c6836fe09679ebeaeb7b91b9d631c0571015d673c480813c83b5168e113e9441c851b6325bc4658b650b94fa62fae1e8e8f35b17669463a1bebe182beab3e3efed2c7f882a6d9fd185eeeda3c5f66eb6f22a83911aab6c90b6a4273f2f307d1f9ef8befc82785bb61a7fec818b57a35c895bfe8007def7281ab76632947dce36c991c494fa6e34b258eab8a8d129167bcff776cf48d57dc679aa9e67bb7b7b97b46ebed0d23ef9c1be93e440a85790d6b06a13c3a119b001248062feac1d092b8afafab49bfb24b478680ada06544e55f8d1c94ce5c904d93cacec646c98d654bf7a71e635691f1b4bc41cbabe18913cc1e681673659464c41c3b75383ea96f63557c1b4507d78fbc8cc7ae67ba9bdec37d371d7b3d0f90f674fd3e7e7d81d5fd31cc51f8c1a7c0d8301bd7de5fdb689ac63ac6dd0710f91f8a0e0f67c94c51e6158b914b3ccc0e5f264fa672b3109fbb48f2a31c08a3959732614a60e444b2e4c00016bf362e268d1c01ac52feb62aa2ea992a68023f8d78ed0ad3010aff179498ab50af8b7644c745515acebf920b91a61d5c2a8d96a8f344918672b17cf2f5773e9e2aefc7e56c7b4ddbc6838d94cc87f6293fda4b4f5377ee44d8f5e5a27fe67430c891c763fdd79a35e419bf1d4bfdcad12b1d4deb06bb61ebb8a41d3cfbcd52213bea9d794c6014faeb8364d0f0a54c1418982129c449d3280e872a103062727b52388c5cf17566128ff93229e9329dd12a2ba3f993ed498d70357097a8bd28e514969570201094cb8203a464035a58538931d26d61556fdc96e26c4383af5df64ab427591613657a612d6ffaca8d3f96e24e0eaf615adf85037465a6afca7dae4377de7efbf7d3d26fcf020ae4190eda4e5f7eaed735361635c4bb71def3bb6f7a0f8f7d8dce6eac8fd32f8470ef3b8af07d8eb486a44ad1055f146ef8447e60b8e15775ac9ca34d480c0368ff6ff47e7a0426bbea273f7d9c2df57cad1463604de97ce4e447061a5ea834ebfdbd2f9fd73272ec65f138840f50b502944bef894b50455686241adbd0841c51710c48cf24caa46358a0fc7be1978838419845538d51d2ad493d91cad4021cd473ec52c67ac5ddea4e3d19aa3f1d9e066779b55a6763f6616e6bab9fe649529e31448e371f2725f37c758bc54bec62f42881d080b6aadf8a0eea45c23abd8c09da8db0e395e70952ce98a46fb4af08940d6086244329ac8a3bb0ea70e2314c80eb65cd1ea28e156398f14929ea06c07f0cf760521e1bcea44f67a7f797cb7f2df4d8d5981d63ec7641777c7436bdde99decfa68fcb185ebd8ef1c003a46d89b659e2287eae28a081d877d7bbb1d8cfab0f6dda9fbc0eb67b7fb195fcc70f165313e8c2eff2133ff3cc8c76308e2ec93147b2a3ffe5430b858f2b9c454025cab6c1931ccb8b5f954428c783412fb3e7eea0c0e0497af20b5f700b369283922176ef237b62450662dc53476447b931d90b8e34bf606a5ed24d68e969897816eab94d4ba5c00e5cd9651aefa8f723281d6e6c28f734e6c717d275eaa76d06d25e9a48dd5128264d660febf01bc679924d6bf9c3bcf9b04f3f6c7e7b2fd4fc3ef646adea927577dc7aeb1318ebeb34f0da908769e408a7c7b0240310e687336204939e5c98c73251ae008497e012d78fc961206b72105a872f775928cc97d2492f8c95ee78cb85e515c3a650261743e009a75629192428911cdb92bf20e9f550591a404e7cd35b00cf2689ae90e003ac4d90dd41b1096b7ba6d387ed0c6c25dcea7c6bc50e6ec5d5878e465b7105986b371e47499f51f0ce3beffc6ea4f75516a6a5cccb748636733dbddf1e22ed7569a0238c3b8719e3bd01e68f77d54def0667c7d5612f04bfef6c33967ce14eda38aef291032db94a89afce299adf178ff470397bf1a6315efd5e5774c3aefac7ae8c5ee934bb1068f499b671b9bfff4d3509367994bef0a0ebecd0b7ab5868ed19f2ec2a2160e6979896f029879164124d3153d95e64714e8aff8e97f8326604ad43f8240b5f9b042688a58697bac4379bfca15edf51ef3bb2eec24e778f27bad7776463a3b767112871fcd4a9bd87b6074983824bdf85c53ff7861e93670864af4327c9d7fa4de74dd32585f4dccda85868d832a5580c17c11d3b8d06e544b343e386408d01b251d2417ec7ad3c656cb2acaada28221ef57ee9ec204e7500a70596cf1ce2b01a21bd902edfc684fe9102ae2b1f976b2fd9da9dec61a48ff3fa6e3a72ca8b36cef63ac65be09a2377be53a01bebb6d3b3cb9be7d53d0203e3fcf33077e45d1d8e98877ffce2a8fc2b4fca7b35a7cbaa24cbc6325bf28ac78dcb88ad7060e47b93021df8b2ea4d2b386d9d8586b798f02c9e2ca523f387654d54fd08405458aec16b1bb076c5137dc5b5ca031820fb43bf0c93d04490601036b9b67e9d58809d4f87e7d12a7dfd7a222f47e8696e348d4df59e5efed85036e51def78c76399179f6c43b2a3af0ea75675c3c2b4f4273ac8874937aca5c2d9af23637deda1d3c3cea9f75ef6f29794bfd899bd380c61afc3a8c56c450459a7318cbb530c82ab4f8643e0920f439433adde1450c262798fd3f8479162ca246af493e2049b414ea2171135c62b0e95e94824ad708657b3fb56f58b46dc80c49540b51a651ceaaf094aa81205db686bcd0816b2f4239f84b290a03ff9138c3fdbeb1fb187aedee39b2a870b6722733672481805f6f6b67fe8a81b8f23d0071cbce38edb5e42b35f93f6db5f3b836bed7e830f9176c3baefa21704bb89c78ecd377f3d6fd3ef0667f737f3cff778a7c17cbeffbe28191aac3921bdca9711c2092644140ca880fcc33d738783b0dd58af5d751f2e65571d45e7f1979ed76931fe0052afbfe75d4c3e647b711334ba05f1143a691939229aa329955c135d932768db441c0a970e89c453d6b8ff2001a45474d2b24b2efefc534d952c70ca65d204bfba8c725a645d2f30a3fe81187514dbf5d1d17e48fd4cdf4eafececa5339efd48cbeef8b6587f0851508df46eb0b7a20fbaf7233f72c3d3e9f9b5566c7fa58c54b1df51dba6381e8c4bd1035d18fd074c91a919e1c0b472b5d38764486a7f6d2c256f8ceee0725cc098b1a831b2bc23a621136cd1d76980340fac959a3c9450c6c84bbc1925a49cde9f1ddc4d0743bb0b523bea2c08b3c0a27f31da3d9bbe5a5e09bd729baf63ec0bac733d7eab161e853629d08df5a73ded69593a6f9e573fb3b7f7cb18c4ef8d8198c2f212cc141e930709cb772a0878b88c5ef303029f26393c1d0e94e1150ad383c7b2414032d8c892594b7e0812578d89d0a3785a2afee5acba796691af3c44fae47b715b0719a9cac58438ad4f58454a3930c27f841870619569eb54ca4ccffca47e8dde339f8dd2c5f453db6ce46137e1842f9f6d8ebdf793b9d1d813d6fdcd1df5d9ceec85da6d7e83a17439ad4c7bd29bb42b77ce268b543736c6d731dfbf58bfab2274d717f7afe44506c5e073de4826a163103c2ebe4abc18a5fc1a0d1944d0301a1c25f1325004924edc62fdfc69a5c9a4ba62ccb060634af1c880a91c3fc972aad8c810b79778fa32a5d096e3966945c028ac6c02a042a1419eb6019cdad341c24158380a29c846ce7a1aa6aadb0a4cd4cd268fdc1bad1169c3da8e4e01adae9ca3db9d8c0fb6aeb212fa6c8d375ec3f89ce974fe3a8f27a5df60ed3b530e0206ce6b7b1d6323bd1be71bcd49d4bc6de93dad1f7fe9f10bc5efedbef7defd0f42255ef7562d2f65a8a26b092427e8859fbc233fa35d1f355dccf365b9fbdde72f5a41f6fa5729174788632f4f803e8f8bccb8a00ef1f01561a817719380a165a58537b5d48408712b3d324db0dbe25d809565d19a1e7154ecbdb587cbc46520f52aaaa918bc25b4f12d58e0ff51d0b8be9dde7cbf1ee8c3a4acc4f26a468cf10347d87ad1b1af91d70dbe71fa831a5ecc5f927edbc74e23e8da69aff6ac63848d9fc3d580361aaa67b3814101cbf8134d7ee284b631bc835f80ca8bc1a141417987d10d955eb2d90d995d936759ea7398f223b18c79dbc41f45e5215d8c96d9f42df3e5fa6efad22f8ee686f93160bcb5dfef9e534dbb13c2e2ca03e9596055b03e6e74c8eb18cffdd8a5c74797b351a01beb7d7777f3bc3a5cf3a3f2b82ebc0b6fc195fcb9d094e12acfb9319b84c2c5ba96df8082f756a2a072293e14e57087479c49066fc357a50b367762c3bfd6e5afc31554e226ab9e98af6d56f8bd8aa78c7c5f9b92e6140edb6e73bafc65918cb0c4f44946ab2b8d2dfd4bfbee1a8ef781b9bbfe0c4e8fc7dfd84cbffbeebbf73d62d4613acd7b1c427d73684ddd7da3b5160fb4834ea976d502d0fde0f18201c981c060555d337ea214b03ef69610ef25e978951f674b278f738c6510892c13ca44f28fac9ebf30a6290034567770c241a6ca4c16a6a07e76d2f505b718e50ca78cd1e04920a1ce943280c264fd29170422e968c45f11db99a329b254ea00ab656d1f13734d1e1ab055a0bfb62db76b1c72995c47f9d4c0c57a0b6f6b7b4190d6e8b037ed0f48b49c4c107dcaaf57ba9183b1db77e20e3da0ae01fdd9eca8fbae747afbcb349e03e5b2b08b0fda2d5d4acc7e9d4f31bfdb866e1ae9a675e7ceb0bf1e3fccef6dfd6cdb7d18fe0723dd3ef84389dceaf8c7d0a062d57a1c69e1bd15434151941fd99e555fcc27cf52fb600044b11cb6abbeedf88bf83b0d53d74572c1b8ce425f0aa241b8c27f5eebd65b93f94858f202229d0d44e6846e7427dd7037f2f8047795c90cc948102d59544729b8c08323932f41b1fa6f2867d55516c169cae456daba3a469117700b5b4e81f5779a01e6bcf6598df42b466f0bb1f4b9dc91759180ce7b5e7a0f3d4afea58f3a4c9f064aa3d0c5a0baacc648d8fa0855c148ae32080a8f23da462038d495a685c67d9088a8791c7bab52bfeaaa8e958ccdd5c38cc5206bc2dba65c0a5e81cc195f76d3a9e4754134be3423bd27319ef77b37dd32fd4e88e325dd4c33ac7fe4ce5f0af45dd5eedbd2f179759e3d782b0bb577c3cd8dc7f40182f98bffe5c932dd73950f0189fe9707b5d2eb3ffc2e83171c79c01aee7a44408b545cfcc4e06d45c3bab251a6ce11c80483110965114c84951703d197816d52419999138d8a4e982071716118475da254df65f382740df7ea37f9ca980093290f920eb6af25cbc9e9c7b79d09d8b0d63b3cfe7847fda69b6e7a3c6d7ea23ad816a67b5457f64652a029adb14db3e5e5f767377d13e6f651dd06c7636e0d97a443d1f14a359cc4962972db27d136e61abde4c3000ebf0c210b6784644462cd3c946502435252cded0c950c1084f92dd5991b58f1caabba0cbc0112c273e45a4d815357e03ab080d66585ad5d61e436b98883f2c3048560d89ee1956f29d370a6cdd516ebb39c1833693538e3bc53fdaacd0f1ea511edf24096901a6d7db77b8ce3fe84efb8ebb697d0bdb7d2be938e00e3d888076df887bebe9ae97f16d73623bd1be763037d1ceebbea9be7d8bb91fe99b6fbfef4ed4182b9b97886dae099e294e2aba696d38cecba241f9aaa2516d367ee43a8c3db7850e19dfcc8eaa152cb5d04b45beb3ec6de0b108dd0511a0d869ac24c7a64dbd94910234948940bb457bfe0b2535441d21c137283d7803f272cbcc0334aa6a5027dcb278140d5e5c35919d9e0881e68e7d3bd09c68f9b5d635bddddf4be2b1b84a3cbf1e3a70ebd73d60dbe11f8831a84a6cfa28b57d7837345cce84982fb5031c6b83401a8a67c02d2844bec9352aac46b6c1c87a48f86cab1504e323b13b178a86c1ae56257a42e7411e35bb7e16b1cfab0f8e05db5a595092f50bc297dc72a6b2fc76db97c131b47c3833ddae71c3be39452ed891cb69beed9b3a3dd74887591b9cd5ddd6de7d5619b1f71912777c502879fe4c5ce7ff2b02cd76d8f7c89b731723600524e20121b1ee1e38a650b77789ed45404d70a8ec59f3f2a8ba15a9900ad6616f3f30fbcedd41692f583a8d5a7c6ea78cdb180f8c5a9348943df05863fed99ea546d91d85e451dffe0dbe928e7ca34abd3b533eace5987bfc9714c7bf4e3f5a99812342555ebe7476b6abeacf872393c2c2ab467d08ff75f12ce72f9e4959fb49b6b46baf1241ab8d41c03fafcf459fe70cc63d542793828939c99d91993e105200ed368ecd6ce960926a95c2d639e0485a59215f694a3842a263483b8138b7066596d6aa470982fd02994744b8b509820d1236e1b5351ea021e3fe6782a178878e0c8ca0c4f19e262ce844281dac1118d066f328227780b0565538146c25587ced8141d9dfc227676d78ddeb3431dccbde32377bc82b7c9be9ef1c8ab594215fbcbbff40f89a6d37fce6efa870e33d2d7b10ef3e1ea833e0da07f9ca9c3f71de1eef7f40bcd6738ef181e209270e103af15d6e05429b8e40b2f53c01cd21ec519d82fdd7e567d3b153e76e5eaa152212e74da8d7bf9b6b7fdbbc74317de8823db492139b04b30f454575466f165e4ab261dc1cba86b9a263a84b07218990540945103a557320a26331ec2c47543b3c711d2d4495c8836854de68bc9ef54811d453b169f4f256a9c4f3da25633d824c73739e48ccdd7775a03bb58acbe48fa6720ea3bb1dd2fdc0ffe153dc0332ad0cd4ed331f5702634e899c58c4a8d5f276568432c7ebb745a565ec17aedf3411660ad6beafa9496decd2a187026ad009326489ae558b4b6d91eca057f52898a53198bdc4df972ece4175b75f1ca3cd75cef475d8ed1a583bbe9e33257e6bb88ab94a3ddf4152d2ed4503f06b1edbc3a22fc3b70d6bb64c1681d798f84e1e50095117eacb7aac4542e5294e99085a24c18f911522656d724403842a64e1179656b07297342a50a84a549a02049d34f59018404af6a6b80241c633f8035e75875954156c0af8d84574d328f78d005ae2ec9377d39b9dfaf66b464776cd01db89338de51df9fec7f739162a5d7d5f1ad5b65c8d93fedc3c9feb06fa981deebd0df8cf73cd2e7bef9c5d32f5fd27e3d4fbfc6659c7209843df6c2ed93c7d5f6898ce2640829c28caa518330a00c92d191198b513af39a1e239d91aa9d6af2818f219c5d31275d89d9f03080b5c54d5c653d1a648a05b60f466e1389abd55f75731530387b1bc5afa30dbef525ec6fbb522bed09027097f8d4b11c069da6248f0aca1f6aa878ef5ba1b6070afdb53b3b93cb53dd96cbc0995bf236933e931df5dbefbcfd27e9d7abedb90a22773346b4b0f7fcdd7ae2d8bd3fb969a4efec7cf290232ef7ffe59117cb6e30bc775be78fdcfe947f6154772e2467e784987c445c54cac7d27b3e9f3e4b8361cb58471941f73c54da8fbf6c8efbc54243fbc5bb35ae87e1561306f429f96b3454564b019427d14d929296234f8e159a21a80950754c7acfc988242ebc8501f45fdf72aa2c13c4478ace710c5a2e65c72f3fbe375dbea7725d6a3783afdb7d8e1c6639c6f99e8bed031f3a1a3f495a48862b86fada2434643c0801ea3ec946c373879eabeba0ad9e74f1584bce8d438fe83fd2cb3c11805f006d68b354884b775d4620cad890a95ca53956b4ba47028ba272522070d14149270f30f5fc302e4213cff8a408ed6ae3274cf0ce266bbbe92467375d5f07cc5e37d28dbbd0f201d2cdddf453a7e6bca5e7ccfea167d3df70eec6cd761fb9cf8c02dd58ef3bbde3f3d493f9f487e45ecd0c994e9b467fa50f467cdc79df02f074b60f6068793a8c1b7b686472939e0751e5e1ae835289928134c0ec110be5a20483022e96954120f899accd931d71cb88cbacd822e611316e6b08dba6b4183cca7339a5b1ca019a3a004c9aef8bf44c3e32f2b1067c3fbd8377827bc14ee777bef39d4f984fe6d7d989b48006d95ec99873f5693b2d42e166a9319b1f7ae7f9b087bd7b9dfabc4377707ffa863744c70e2418722e8100caee45830a95b121785844ca13a99d1383e11c38a1182e06ae8325580ce36415a330585961a598e51c3822bab6a3edc4e998264a296194a7305b63ea18cea6092b5713302a5788b222781ad9328b1036288de3a9e7c0995ef055d6e234c62a2384e24ec1aaa3aa09728302e676ad758320b816cb13247e65b2b75db66ca9aff6abd70b3c901d756131d2ff35d47d655a927ed2421a56cda67584352421c83fe6f9d54f7b6cc5232cfeea584b19e418903b9bbff5961d1ebbd077834746f2cdb99322b91cdf3033fd0e2399c25f0c88704ed4a54a5a56e48375cf94421e81199f551f536d63336ff291d1c7a32e741a8efbc9aec9f38b7ea158c8578a417d2d5de1d146db9a7842707954e695f2a1bf02a65e488a3c9d322b98e800ca4495380b03238a2ca46a88c2ffcaac03275ce4d5c69abf58becb6039e78fb6475b5ecfc8e26bfb43a42b291e9f4fd748e7d777de073c0f560023f59be8dc49e911d76948c4a4d2b1ea06c35ca41989312a20a07a5842d6dbb9c86e6326aebecbad6c64112ba81908808290b1730c834444380188779d6ddc24c72a936c7c4a929812c1038082c5578079b8957bf0cb61377db587de5754c0b6b133a4d1ae1f379afdbbfc7db4bda46eeb6efaf5e76edc7a938ffc074e81f199654b8f5fd9b8fbe9dd7733b5ff561687321d2eea421e8d3ef74ba5f26e78073f21ae6a1ef9542e6d9a43b9481ab026532efaa682e1e7d253419cf8c2dd747839d828acdce4ce6da4407c1190924765203a90cd1fe500f840f472c6237bb6889cc84cc74123024d5e0c63fc941788df72b9525882aeb9da57186f3479626c0d645b643679713665cd4bbb6ab345c2d642c836da08e8a75dc617bd05edbbe7c8e0bc7ecbb9afefe9e9c2b473eaeb863d5beadd58ff12dea92f9cc370c93988fea29053360873405ac96b44968a0f84b437df540741be6c897e2150e8c003214c011a20d8298bb4c86c51d0947522753a2d9eaa3a8cb49a2d593f01fd59ba6b77ca0797a96958008093517a85c31284b46aa1c853ce68d2681338f327eeb4cf7e1be4e227845bd9f45b04e6cd265fcb75bb3bec93865ba0efef8efa5d777df8f10f7fc4c3ff801ebd348212d24803fbc39f7d483bedf3f45d18e1bfd68d74ab2d23bde456037d4b53464967916fa04686eea8cc8513ec4632c72b3e2cf5fc77a8c3370eb90603b46cc31ea5ee80179f416761f3f697c597a6d7236d970f203552ecb3ab6e107acfd9f1995ff9b18f85313bfdbadfc02f5c6f397b2d8de77bed72629343124227fc3eadc8ad2e9ea5778c32c9dc881c9e6610e45eb335fcd41601b508b00093a1aed16f2a017d21514b4fadcad511b7200a6cda309dde444a732bdb8eef915784d7210cef4fcf8b19571f3a627259150083c75ebeb8613a9746ba4d60e3e125a18a74720cf0bd844e8655b0ead5466b737459e7673cdc49a4888682bee08e84f0ca42cae2a9bf8dbaa3e2815e873a38a9a9f9961f90b828302a0e7d7e8e537e148eb1e4e0f5714f5b02ffc67e36dde707b41e963bbce9a5390df3bcf92545a70c59db4d673b9dbbe699cc353a4ecdd94d3f53bbe91ae9fd2ba4a271ccde70b49bde287ae1797d87776cb0f75736da1bd8ea87602b78afc58c84afd5df7378511e6e3c29b79b2d835aa6857d883d7a4968f384a35c5006b9226001b596e883b570096e0a059597a67fc06dfd558715954d91a621439517f943765265930991fa671b6c7a1a291efe6ac7bdd214751b586fdb9b6d1e7dd9dbd95970bc6f75f3cf87e61f88a33f2f707e74c32ab54ba3f48366d9fe34de46e004e125170973397e7cd95e56e14ee6091feabecf9757f4b2fafdf84de83fceb8d8c31e7b41693ed67e76e6ccab0e9ba15b4f143bb9ca110e0203e418f0933d12ec8c94386c5b5b6230638368f0a1a57870612697b894a91db7626411069d030e6804053f28d3042e64841700b606e3b6a998d872b6b34d28d601b0f5994e694a10c14d31c013ef1d27ad0c72e16c9b75098d2f4ecb0db880584ebe1ca0adc75f4e6f3ba57e16dbb71b8ee0dbea7c47fafe62fe1efafb44fb5e64a4c776c5e629284a851de295834c52af3c68a46b309ec82eba50ecb00fceb86e65c05f9538abdd9c553b75ea940419ce55df577b53f802b8f09ed9db8a68f214bf4650f9403b245a3e7c2c54f18f9496de5116d3f9b3ece60edbeafbfc196e1b7a8326dc3fb9fea5d293a32f955e2c74fcfae73ce75fcde6cbafc320fbb8ab72a7adb022f490a499ac425fc3d0b16702557496a755f81440b0fce29e7c1e2c4160b87e6179270a6f73995a80e02d7d4262ea504652463c2ede27938ff3c69176ec85b4e9fab784195326b0b629dea696fdfde1ad4d1889f7b0665f9d4fff62acf4eb476f0db1de73e13038afa2ed5f9bbeda67c8121ab5c648075df2e5e1465b89ed2b6e8bab6b4cd4a3c66b0e660c2cd4148e63e34236bb94d03e595c9c2f2c9792c280a164033c14f21717041600c6a454920a595c18efb0eea64f7e256534d207f33c2967bd74235d2076e7b2b0fae8231eb15646039d5fe4f3faa3ddf435da5c88916d476058b8eddfbbbbfb27b0d5af650129dbf12bad2d9f855b61c5ccea64c8c0a40ba47e9247e564191bfe8eee27afc02c6f8a305e80a722316974eb141953aa523121112e5aadd8ffd4433a7a2cea8bac2c5a83d0b0b2475ec0ad4f7cfa951ebfb53535592f30da496983f848c0681eac8eec18e56289d1cd3f6c660c815182f9db1d6f7b7912487916c9deda16bbd87c8a44d64deb610068fb3177cdcb28e720c26231e78bc1f7cb40bfdd66f4ed74827d7116fd62dea5e2381cf21239a5766821b81a3363cea5469d286c062742f2c0499b30620641e624a138293031764c73b4c22e3d8cdf0651e6ce608aa342805a7f522d1897a3296981adc065842a2ce45017e962ec8b84c092af6c5869bcf8848a79d2e65447979d33ec421d850126fdb13de975ca88a36e89418b149c9ca0e85758c301b76599ca19c903603de1b09d551652276fbf9da32ed3d9eba9fd64dad33b17019116b6935c91116009f18b84de8bf045183c936e5637c2bb756e5a77a6f5f07df987b5f5beca9d8ff9dc65b8cd1196afe401f466863e36040d8e51a94f828a334e18e81c7e982c9f5989edfe1e11b62cd694d0959bcfdddfdc4b947fb1d0f3ebbeeeb9374199273351bc37bc285df36b7bec4e26ea94a4f52b698d96997c543fca72d32d35347038e93a926b2caca02df493282005a319a8a7a496728c9923e57333b4e25d6b5be222ec5f3932dc9cef4f37785fe7d3ffcc2d75dcd8f0ab9407f7cae4f83c4878227db7afd101c59f31c3259aa497d48db6ea5fd3f48aad09e81c9e7855c668cc6e001d02e3b9cf413c546ef0f912e190db04a9e17604ac2465dba5e94fda8339e14a2d78c0485b79fee737070303235dcd849c0ee63afd1dcea61b1eefa6d3bcb87e0b3fbbe97b7bfbdb76d36f38da4defe4ba68fc036f8199cefe19ac751a0e2b9dedc214def5ce904e6356dd5efc07648272ace900a88790a7f02ff1e81f814912670111c276c90296bcc259bc5cb21570ca8acb768858a70c91605dcd60376b868ecca932d2525edcb6c312e4658e4a3d2929a2e479755d21ca824ed660a8478006294a5e2ec376f72ae9d010f2f6223bee797bfb527dadda6c637a63a37bc7933219e6438df48e7c1cde9daf3f64da61ae21b0edeba4a17d07ba147cf896cf4d172385d11a4398e6c4593bcc303584770290c935d965a8e28e1a1c275bc724ac67266173842ec1703005e067baf5182c4e240e5c64a7b19823d171d6f97777597e9fa44f64aeef75743ce2d6857bac093ce00b1a63e172602c9c8a0bbeb59e49ccbcaad2fdc0541da1b051d5efb40c3071777a00f9ad30dc1623d75b3bebee9eb37cf068dbceaaef4767d2fa43ea7d691a44536c6f161faddfb6c9b6965cd8d6c9c7a7f363ff5423dda32e75dce5aa18e9db0cf4f516727be0f2cb63a4707c6378eafbb037be6c96bd10e35ff4455f743be379ba8fad14963f548849235a9c822c102e42d7f807d4e32fd3698ebff04af5e138513bfe925dbbcde32f17ebdb5fbc2df90ddff00d7fb1bbb7ff14a8f696d21f322772e80c62d049a76959f9d634efd6495ae5cf092b612f32b5c5844b229e71fe8c46940d395ecdcf1881cf22bdb068327d2eebd8cb30481ea870537d905e3695dc5762e65a2eb77de46898f326b75c714b842dd570d158efe107db472f3f3377132054f55b9d904e874e31b1139578504a2094887f7ea6ce68e8976ce82f00f0ee241ac8b146062dc391b1f32caa5ab29cd4cec49db1031365838fb8babf164b22b4a63234c4ef249ff98536a5a61a283e40c63721748ccb30344938fb65db6efaa46da6f7232f8ed31bfa7bd38f76d3cf4ed00b28b7efaa6fbe05e6d39ffef4fbe0f93785e9c39ff29cf3bbfc287fcaacf2a5dc488a3a888061321328033bb915574e0092c795217372359d70e933f0a53e129001b55ac99510858b792347cf004b7bcc374db396799f6266585c785cda499df8ea3e3a92faaa01d5fe965279cbc5dab197d5569cd352a626b1e67b11096c5eb492371c2ae1f99d76122a4d4a2bd250a095e4d207693bfd00c68532bbe82b23bda335cdbccb56270c7b56f9b7b746f8ea179c0f933ad65df754ea457efdf0873ffc5406fcdae287ec93a4c7f280032e7bd53b79cd2ba609cb417999ab0c46383b5c63194bca863dc4b0c9f8e4c7c84f0e0c1530d384f4afa4a34a8b80f4c5e43443ff5e2af90530bf9c1dfdaf6315fccf40715956974efcfcb823400170e8d9262fb44f5c366b8dc7498bd0a45ec128475db6c7b9c996c4a763b5934a4adfa50b22e181854bfa6d76e2d74de7d3ef43c076b619ecfd41098d64f7d33fc9eeb646747f2ff9b6dd54c6e5f118e96f02f76f53d375b64b57af9222c5b81dac105e52e273f907647cb20cf4da457727ddfa2d71ffdcca2019c3f7c5c4b6368fe12eb43086c887a5a03c2d59eb9cba348e7a253dc4ce2818cec213c828d028d7c9b37a9f3d006378f43c5bb24e1e38fe72724dd75c0c34ed93e5f39ef7bc7b1ef29087be9009eb5512b5b837148e5096dc4316e9aac085bef8500491e4076d23d6198dca96d7237ca55384b328570a384e46563a4cd0bc81a0e5f0e024477296ef71705c8922af8385bebfcf6b191327af9f55cff974009bf3fde9fdd88b495fdc76d30d63fcad663d131e7cf76c0d6e27fae841e9217da200a50a91a6a30daa4fb28b683b25e4e04c374de2b572ea3a70ab2b634410334f4dab1c04d6420a8f98f11c8dd46f3d24945c11269022a4110b7cea8f42ad2875fcc1fefee4366275b703b5d5cfa29b447e76d355668657bbe9abd7c9216bd9377ce8433da6745576d3fbb304e2c871a5a3dd74497151babe033b7c086936fb990947a34bef744d91edb8d233f06f5261c918e0b22669de7d92a735816344133349762f2c0a824aabf1b279fcc9efe6445fa5b48b61525cec92ee2538841321e523bb29036494a08025336977554a41d2088b27b5b77013b8ca539e6252cd0ebcf1a5ac00f71456fb0a790dadd56dba9c3b59257aec059dfeb81ccb692db01db6462ad99e1094fabb5d6557a0c76166f80af961a16b5a23daf1171f26754348b25d320e86e0dde950126a469912946162fc4aec300c4341e68c7b3112df61018832fe1c1afc18cc3290b3ab59c23410c21e27216a0ee9c058cc8bc676823d370cfb5eeaff2950bf00e0ef00d76b0179cf623143414f9ec183522784b664ce9d1271059bfa6478db449d2e3062d093913f6708db452ccdb60071ff2244e4d020d28c9917094a3c10490724ab13619dbe103c90d1a56f8268cf3655c7e4b1dab4aba45cef3d71628763ae87babbeffee0551c73f9c99df9ec3f03044e682e7efb639be882fdb66e9d61bb55898ce172f2539ccb7cf3d84817ee8119e9967049e1d9d0f5f3e9a669507683ddf845e196f30f49d38cbbb485a8d92f94f6fe41e38cb6fc45870d87e518175918e36838fed477d53dfeb2b3b737dce63b1b9d2e349afee66ffee677bff5ad6f7dceb63ef533843ed475e59557deb0dc9f7e1b72784ab1a9071025a0f4958e7035842cc56f72d15abc4aa1acad8c46e91bf4b6356362ba7a4a792dd96cf290a1e3629686a983139d04dee9f45da46e7c8a2ff69cc9b89ab8909de1d80bc727fa6c0660c984901ea3d04ec7404f7ef7cd7bb0dd8fdc70c3d3e9e71740a6e8b5e2506844bf6b41698ba013f472c28f8606562249bb38c7c00920bf4a0addcde6171d0474c9414949bd5a11291117e5bb11643463434039891a352d6395dcd493c5af857dc34b10ab7b2db47cbd46787615d61f2120b59c79e39133155dd5c76ae2eb180bb25ef5e2dd8f3e5e47bbe945998bf1da370ab6ecaaf355eee5fbe5f7b0986c4818b62d9e0f31cc6df970abf9da17b549236ce353e5017e8dae925d815330721a017d237e5d4f174f39e547dba770075f15553c29a02ef35712acdce4f940da105ce83d02fcc483071eebaac5b2716ba27e402ce791328ac6da407e73c04f99593def91bd85c8c999e974b87b0ec8e0ae61337bede1dc9dd94b6cead0a5405217759b18756b67e8a7ad8eceb745cb89af4d3deb5c787f2df94b72479d817ea104f51fb51eb2fb5a2199297ce1e893ae7a0fe92b4a9ad03eb16f1112f90f1e62fa16cb5192a4171315a48316b6ab7299351482e52748fd051e6cba1e85ff1d30e1af32b11e306969d3dfb24e9933cc81dfdfd261236a1ab233c5346ee75bc5006f232cd3b82d38642a852712265ec2f68fb6478208f827cef4cdfa890a96f3fc0a97f866cb7f4c9b9f61a9eea667ce6c61ced584effbcd8565f7fc099c457ff5e94f9ff800cd7825c8396f5ac6000e40f1bfa2ada97e559f5cb756bbd3c0b44d7a23e4ef3a76e2de1b763ee9f9e87b0e3dead277f8bbdfdb4abccd7df74cb61d7be9c6b9fec5b0fbdbfb1d7fb660074f4af72b8112831ae76485c8513e8e812e4ad2c19f4e1ec702e95149e4d277d5dbb67a34e2fefefadb5f3e367afb9b6fc41700004000494441544ba76d2f7fbefb48d52be9f56fbcf5c6b7fef8665bddede84fe67ff2939fe4b8e5e2df32293d079adde63c2541e5d72caebd5b854047a12b7f61e4a2adca258bee444b068bcf492fc593c18ab85254b0c01b62c6088875e5815364773abb69b3adeb072b324c017920c75e30d257050f56f0794fe141fc673a23ab8bc2a64524889158be481a5d45ae294ea62a90222b69c0bb5032c1e35ef901282d554e1a2aa1256965acd4984509516719ecea46e12d682d7aa4242816ebf1ca2fe262edc40050df6aa0a48ed9e4bd4ce77f5a8ae818c6faea634614106776d3b53a0c6fdf4d3f99e2be377dd84dc74aef3beaeea68bebc85d9c14e8c67adf301876d5a7d35f08ef359b43e68e611cfe533fc08739ea155108efd6e2167e0f4fcba7f527e58a838d87179117195c29a83cc3f274745564c99cb2158225f68485c1ef849e8d50e32d0d59341c490d22a3669a869ff6e3db1ff2ed5b15b62f2495fc960de536baab5b5df72b76d6eb78433d3a7db178a1bd483ff4a8287fd59cdc1da88d1412a89f6be8c1a6e969c19b7313b3ff7adac15bd02d67dc06932eb91df55b3e7ccb5361826b337986dc197942456089924181093205487b7669b2ab459e8c5df36ed4734a69e4d7408ac33f71d4a4a0612f03c9fcce0b2de2d9e05f27ebefa0b05fcb0a168369bb6357fe2a60876320e2aea9a926141046b882dc8aaaf1d8ea04da84904405a01a1941eaec146191f16ca37fee4841927a78943e220931046cbf7d8ac05ba7f8fdd7b89efc380cfa2bac1e736659eec4805b37d6af1ad61f274f9e38f1b2873ffce1ef6492fd2fd0f315547275da0a362929cdeaead8545d36ddfa75e6194210eedc9bed7da7bb7ef79ef0dde8270e3deac2ee7a26aaee0751bbd4f9f45a4860b7afbded45906ea05f6886e5b88fe370ef0f63771b448d527574cb1034e4be3abe4c21d5330c844b54926b39f528fcf3d431ee6de1936cb78fd3fb42cdb4de9671fef918fef7eca4f362b3c769f14de7cb1f78eb5bdff2f637bff9cdc36b35fa04d9db8e31b50f6dde8dac3e9d1df577d7439dd25808692c65a55fe36a273ce90b6367f26c3cde66bdf07bf83e635265cb40cf20800b3cc115c9887c82eb56569fefd95ba3bef5fb61a3d1b11784a852d7afeca0ef4dee592db0bbd127d4b9dc4d4f2ba7cbe7ea8786f8eab1fca29fa421749014d2d13c83021b2f8ff402918f0d49da32caa5a57ad6c132913cf00aa651a39632473cd23d6589241c9f70905bd67fdb40ae15d40033acf2801832fdff9b3c3610c3427bbb4cf664b68b76c6da46fb89cdddf4536d0c8b25f3ae7b5ecfa3817ef42ac631252fceb006e55807f51de1d9ecccebe1b0d3c5dcd5f7ccdd0433b7cba259ecc2d5614fb9d21940deacf958be553da9bcc63292709bab1b5bf70ad88450107a39ebe067012a897d2164c3590f68926e950896cf8194a13f0011b0556e242a4dca12802e989545f094bd824f325f13ce6b0c8e2947112597bebafbb7b7c086fae01ef188473c196a5cdbfb6a9b73b4aeb6d1730333ed715e50ffda27db10f91e7de360c0380e8cedf871fac1f025b7a33e5fce5f243525bccc554ad4ab83ed554d4fa62bbfa6f48d0aa022ce53c9308b86b0d075ecc5102e1c231e0b5881f91aba441d440690c02906f4e5a0f8090460b05ecdd9e660babf45fa092b0b2313a976cbd8261a97696d5d7a43db61e0d6e62c9685316e59fa64b16427cd8b89fe7b211e6b5ca0fa71a55c1a5f7840e6a7b54dcfa755aa61d7215cbf786c3e7f03871f7e7cb9dc7ff9dede999762f87ee37c3afdbecb4f5ffe7ff37adf3fa2e887f66793d7d088a7a415d5193095cb22c4c6a66dd430f491049b90f6d9d7e96916312f3bb973e6ce7e167df3a80b751f1fff7a1d07fdf561d8f610e98562501eecdbc194bee0605ceecc804253f9a438882ba48e1464e01d087e24fad5db181b41c9f89386443cb5d7301c7fd9db38feb2f92df35e00ff42b953c10ef87745ba22589263fa8cf96cf64737de78e393ec4edf4defe744bde5d97e77eeee2ebe11d5f0ebea17d95d72cad77a591089d33c16d3b59e96fae8186003efa23b6548562fa5782528f3191bfc5a503b26a4928d2cbd257b38c0eb68f3410bb0b2daf5d37e99b4efbc02cb6b191fba6087b6de1ee20bd4bb818e7fff66bd35fc9f9bc88ffee88f7e217dbc2e149007435362f8aae004e23162e6996836e1da6810300c1f5a9aaf6a916edda548109b41ae83610522213d9b372955256a919bd120bfea2b38b0464eaab83bf5b5884096f843866edbdf5fde5496440dcfb6b3e9c9c790cf6efa31be42ba189f4defbbe9ab5768ba95fe6779e3bdaf763bda4daf51ba78afdd481f1beceeaadf7beff20ed8f65df2ae8bccf0a26c1c7e969dd527f2249a48d6560460773590bcaa3e92edc941048c846b9b6eaa028a49f086edb980a8895ccaa606134404b006ad92227e75aa38fde73458d24928a7ac44301b2cedf74f3d97a660f7d0c2c0567740003aa6a90f9b18698a857e1f6aaf6adb7a65bff1ef82b5da9d3e28b3540361a49711171ace8d710293d75447b7c4fb4e7adf32899f57351e72f6e59a8d87492fb91d7528fba246c410bc0641c6d1355f7e02a826d662ac0c4a81d44039588e581ba0303c3c530250cc635a3f0a5329e05dcc7e7e39dd1dbd2133151fb8cc668b5d7fb0ef33c09a96c9d671d66d384c4ccad038db4c66da4618eecdd7f848103e7fe4259f588c76fb290ad3f5cbeb8c5612db6861358145386c9171df051f5a599604f03c91c23c213d79056d791550af21f507583c7c1334781c45b2e8901e696aabb8da26adc5e338f017df06b9c78ecbc59a17a73986f06d1cbcfdc36dbbe8dd38b7c8d899be116f127cd5dab197f1bbd3bb81de8ddb71f90b3dcc22f3af1a3b40e2e211e9eec06aba7895f67d1cf2e69214900b2a8f41fc9b9c6fae0d0b5257c75ff25869e88df1977c1654f9f8d1f8f88b343cdf69fb8e1b6f7c2c3afaeb5d10c7d00a5f863aeeb4fc87b7bcf9cddf6d3fcee6ceec2ebe078afd6ce36440a5b7d435c835c250ef22615a32b5c94117414606044e8891d8c87e8d8172974c8dcf20058287d241f1160fa7f7d92148d72e6593336640f1cda6c5e61b5f56bbe929d6b6d435d2bbc1be86ee418a2c167bcf4e374b59447775124a8a5817e88ae86fa26e2a84c6b60f40a9568b9a462ed39d844347f28aac429943dc32d09d0ce5221b238e91e4f6e798283f84cbb0a834f12845640595c67cda67421aac377de3ea0ebd6232889255c7996ffa8c1593a113bc02175d76e624e1935cd0576d37dddcc9e48abc99e78b273760a0f70564e51c5d2f250af45d75f4fccfa9376442c5238b54c2fecbec7a351f1b6acea07a2ecc9b523d07df73e0ca94fff597f95b30d2dddd4e09c2d91d3756e0cd4f41f71c2cc02fa55346b415c0b34ce6fc263714b03942a75c5b6cd8876cb20695123a7bbf3a2ffbe8cef09e19cb4cdff7202441dd3927f7a0e3dc892f5b79f4a31ffd706afb9634914becbd40db30fe6dacff86a30788b8b2314c10778a9f4d19abe071f8c0cb172ca4eb767ac56a23c8c518a82f7ef7a10f7de8e990f1310eaa7fa13314cd0a4dc2aadc6b54322861520a38187abaf0bb818c0e5864163265d70561cbf45d9b626141459ebc5ba7f37d3e68b13656621b1c0fafeefa3361b937f9afb83ed170f16bad7a87f6b4555d98d73ed1b89a6fa8d980adb26d541fa6279ec9a67151f11430065a5aba059c85c45017b20d1637065f2ec20c346b7db78c48289815b89393e8830c1ec6972e99c06c5730a710285bfb9246a6c86d3cf55677ec23b4984dfe210479bb3be83a4b773736c6378fdf00ba92d456a08ebdd48e3a65f72ff6ddf44e277d367def72581c831835d299b8e31367849f3c947cd28b4de42b7733849b7dc1fe649f0f4174c73df92d8ee32e6b4c3f3efe22785f106d297ace93163b3bdf1b252d1d5aafa5924c89e17e82df6baebcfcf2ff0bfe3ae9f9f4cd06738424f2bcb7b7f8a710ee0688783a848e50280450b2c9df386c76e90e7c5f23d5e0150907c2bfc0672c4cf37e54526dd71f3039dfb34dd5b0705abded25c61f6be7e562edc1aa1c7b193d017ec52d572cc6ef4fdfece38319670cbec1dbf5f65d5294abdbe2c592e4c599cf925e3a4ae396162f05e5638631f494cf8d0b2ceaf2f320702fd9e86f01b3c5ae8e5312f26f1d94cd94d0eacb2df916b6dec80de01600960f882e7fc358772cbac22bc669cb9a01ee9b79b84bb3bf60477da281aea53ed9b29b6e72737da7b5c78ffc8b9b028ef7e6aefae5a74ebf1986feb80bfa9aebd91008ff9736cb95381c0971b8223e6e48942b7d923c341bac8c2cd50e77173ed3aa2812915d6ee2f23c3894c49415c63a83b70a58473ef0467256b978f932b61ad4f6e8a363a3ef80b51415341ce5a7ed5692469805cec5e22f4a70ba28e1c74a58dba713dbd8adc99a3ba9e8deefc1f3686fdaa0bca7f9b424d571d12eb1495e6af10fbd5c8114add6700275c02d0ef942e935b7d729f52f695bba7d5c25c9a5e05e2885fba49b0e4bf93612611fb9221c61ae4ceaf094cbce154c91816969def6081c1369ec4a62ab9d13ca674441483d40fe846f7169450f7828e1ce59794d03bbd5cf4be5e10edb0206ea0b4a6ba5c2fe40545f6cd484d43a909d2499ca362a34c54cc1634a68213e7b5965044d7eeba725add6dc2a67c4184c6a79fe32b9991f14c6bbc190dcac32acc1092de4a2924c844153388233ed488d854c84117c1f8eede9931fe049b17fbbcd40d748d738ef8e261d3c2b6fe286dbdb7b089fdf5e3f9b3e3e9271beeff86e74e78144ef761c1dce8c7f788bb151fba0114ccf9865ec6a4884abbfce148cec62fab45e2946600c72ecbff62efb9e73b8af917ebed2f89def7ce76574f865f41e4f3a491f7e3272adc8c399d0eca5278ecdde892c3cd6232fbdb7dc6dd83973e6ccb098dcd95bfc1c05586862ac8330865c7ca9caf295b0f2540b21f249c8c35e56c75f60d20ee058b1462a9037f3eadfb2b673820188aa69da069c2bddd21b379cd974f3fdb28d632f2ee44647293846e12ebabbe943f17310a0fecb68d8d352b5fa826e3b4fd77342a55fa459e8c8f8f4bb13513cd212d8d056fe0ef17231d11c7ec43d2210df3cd2fcb7ac051a48dda9045a59b11d292fbc5369d5af2f78f23438f257b099e0a79337ee4e96f702821b58a4a2edbab3a3e17ecc3b7e19495e6f3be49fc45a3f6c37bd4fec03f051e092a0c0d848b7c3eeaadfcdf3322c387f216f380a0fa39ef0d526ca458c7219559eef3c6e549e2d0626ac4120479b6e3019c375281e5960fe170f459406e533d0467a1dce35fcd467a9c472c8584084a780d2a4ab9612eba004b43d72a70cdffc6c4016f8c77926e79e9dbe2dc43e5eede50df2d5342272b4f12e61e0f62fbbecee7d77b339c27872b2dcf72e68eb74b5267a3ff590c3bf96896dae3626a31618822fa72ad6de92cabc9fd732d3d9c06af07d5c3b7dee279a0b130cba3e3f8c58cbc922ae0c2303f0e72996fae04ba83c70cb6ab0a43d7d4f023e606518a3d841625e98462692510514a7a19c139bbc87e001a7813e36d2e5247f60fdfa5e57aa0d22ebb2169dc67735a66a33bd4c5a271205d09d27f31ce012ba2a17c6a6518a4a2692665c071d058af10d088f879f2e55b4c0a0636a0f1878024b9be22b7896a10e2ace392ef15473b3eaecf8d2ae94a98a5c386471115845c136dafee9abb07b7e616ca46b9c8f0df4d6bcfbf428b37bf9e5a721f3ea7cfaa5b49b2e81a0e7276a3c8a078a0f247a29cc0a91176a36e5c840d430b77494399cff94f5e32f839ab4e47d1e7f395f8d741b8f7df4cdd0e56a6923ad420b64dd5bba89c9984dc617d31977bf96ffcf0e6f24191bebe2e98e2389677885c7db30d0bf0d649f50153529c2af056e0c4f53233f94448643e5aabcaa35cfc2ca0a5e25761d34bd131cff297304d3449b997a13d67cc68dec7cde7a6d477d0d6814c1503ed7c6fad7c079f95271eb78ed0ef6cd81281508224d189b2c65a28fab13d25afd94b1cc78a24f29d38d706205c898d61117292ba0bab20db477303c6624fe28c62a239c78ac5a3cd6ae416e6e7098614c5ee2013f8abea9cc87c18810a0866d6d7adf65ed501b3cf0d5feeeb13af60264165ff5def4f61cc1f09e97d567c783f3e8724950a0df41e9869d9dce1b60f6fd2e8b4e4e94471b63921251697c59f9722c102811f55ed44c2b27981f4e2a83a074621080a4169fc81765e4fd627eed10b196ec440e824b44da2f3629b9b5016254e1b46e1b36285d4a062f79b129d24211a77c6009e23e1085b7aef4d6630537e96f39677369d8c4d0407ec85f3f6481bc7d2b2dbfbadb575694e68458a15eda66bdb14f28d7fda027bdf5aed736f647d2ad2d7fd0f98ac8b1eb0b6fa878713b8fbd70ae2aaf92ab9d6459a691126690fec30e59889c04a85d8a577d0c7898a498a20d967cd3b90a981201aee2242f6f5f992cf9eadce2d5625cb9e21d0df4d1a8f119f6ec027b6bfabfe776f7d5315a29a4b15bc66b5a1a260e8fda301a2413a742b36340dbb7bea3440b85b141553c42680fecb90fc8566f98d6c0275e2fe9430fe35bdc12e2b09fb548312e8b563b5290894c2cde722e473be430eae98649bd431a1c3ef81a690dda9493be19a3b2f8ad91ccd98ff29af69f2a7ccc505b0cf433d3333c1fd97fd36157d33218f7078ebdf8b6984b7837ddf3e49f9436f28e6ce198c55009af10ca380b61003e51e3328e5d616667d282d3e963b95e29a4aeefaa576c75dd3cfeb2ca398f8fbe2c172fa78391bfd0402a4118ef661128dee42abffa096c2884cc4e5ecf43d53f38eedf66181a61484f5f082c6fde29fa46dec42399657ac7413fb5105688880ba7c8487a8532a346c44571c097d3b79bd5dd747fb59bcee6da70ec653eaf5d5a647ecd48af632fbd749d79f67ddcdd69acf7f039f09f1bba5071742254c8560474887eec3a83f1f1f9a0522da154d113c2e66bb1edd9a2229834ad9e48c16c2a345a178f4bdc4806e3d1c667dc71c7c081f2e76e7c86c9b02aaf8f09051c54fee3a6cb77b148fac878277dde8ebd381fb0fbc9270976b29bcefb793259b8e3d74ae3e5dccb2a3a0af5497d947414bc8429e0aefaa7ce9c792f4cfeeee82ed95956846fe5d1f064128886d5655217fd44f8976ff5bcfbd7f5921b15a6859d49aca3b602a9a7ccb04cf17ecaba61681d960840c1a9b3226f3d0f609b6545c16d308dd4de20d2f2c56e9dfd44816d0d8ac9fe07624fe5b25a00a3e306631c2c71db6c02ede3767cf17b55f1696bd32916b28ea8feae30d40b6d23450d619f811094f64e3f95c0677069275f86927de1e5705dd48e817c8904ae554f8d78481aca174385c051b2c5623120997c73bb43e00cc4ca4f524b779b26c3639c5f19330655fc93d771760ba53c5ed8ed6020d75197a486b130d297f33d5778ec863fdf89b9e394b7c3ba30882bd608914d8903aa7171c1e74a8e3e2d90f329cf570af5e24a088d912f618217ea002b7fdaf77a78b004240c027819288579d8d96f423b20a7b136a79791bb85b52e793ead0bdd238f1160732bbd4269a9cd5e4efcb4f6dfe338cbbf0464ab811ee39c77b7e7a6300bd45aa3d6155a8e263731044776d3d92d1df2fa6e7adfddd53f9fcf4d574f3ebbeb747ad9c7626038268c47542f040fd7c11331cca9227cc758647cc8efcadef16c8376025befabc7bbeab60cfaae2d96365b3b3ea77e3ed2fbed6f7ffb5369f313c391b948899aa0e46ed5b23488812c71e47b3b399d9c809e3fe01b90883dcca4b16b775c65cdbf40f6ae27efbd16ca2de800968159626b9dc11a99578c2257ce9826675c2abf16c684a79337577d6a96a6582a61f4e10f1754f59123b3684b37be33a1b973ab7c60b4e76c7a37cef1cfe9d117bafbb51ae0fee5d6379d8d6e94fe2e94d48da4d5fbea49ebfc2aa1e2a08fe48288c2f92fc7b760f2b22328780620f4241f68d3522e6044aabe8c3b8acd71495b48cfc604e5a3c363c854a6ed130daffafc25bc036ec7e162b7a67fdd4180fed558c7e45876d363a4ef7aec65bc9b7ec51557a8bae3face6a8f1ff9971605fa62adf381bbea4ce8afe7e73f0e2e94590dc9e73a1588c18848e37b124cca8fa43e7f97a6ab6bcc8756b61fa35183a92bc390ca64555a8263a2322152a06c45f0d39e94a73dcaa6e95503b86263547a8a99af32c417b569391f3f9d7d6043e591f340dc35932bbee08a674394c765f1912aa88b0a22bb365a1dd0e43856bb77d7e869c8683a6502369be535910fa4f601d615c3c8f5f1ccd08dd22faa600caee5f4791aa735cdca100e2ec485f31c6f8dd838bc3a75643e1958d99913e5049c6c9bb1f30abe0c20c13016f180e5522c46c2adfbf3250f90ead627cd4aabab063ac6e8defe6c7f973790fb3ef2a788c1fa5cb15967aa04b7836515615402f556836427474649ed768c7f992605828b1c27afc248bef85b9f10b64c30c2592658d282942f63db645b63c50211567a0db5a8826bd0e4401270e234bff01670b5c2f5323406a7ed12b53f85820b0fc04cfece6c76ecdf6deea06b9c6707dd8f2b6993c74a2fdf4f038f3fde325e39d79197cbb160ae0238467b1e20ed6f7a31ad1be8dd6837ed627477df7df75f0dfd0ad18d41f7f0030a1d3fc300c3e5d55ef245fe1c1a462f83091c3ea3f8e5032e0218ed61763c8df5b5e32f1dee7c7ffb0bb767be4b02c89f919010a3f53b8c2aad2497b9e174e0a00d42641cf23d839c77c07fa3876dabf71aebb22c7bdab753c38b29f7aef1e2db3398a28cccb549aee43a2d21afd35f7d651b8c0b3f7d3772745bd512abaf82875c81ddd7003c243bc9bc8e3b4e239d5f37e8cf56e4f392f7633ff6634e5f8f6b9646e813bad3778783def323a0fe8118c59f452faf4913ae4dac054eba25557cd2cf6ce3fe198ff2ef1ad7f4e2f794111aa0cc255c5cd8a6b64a26abc624e3189cd691b6fe316d783f4983cb6e7a9b1ef6a7fb6d37dda6c22130ca311f208d8b919e5069b00145027d425f4f3d8a5d6a14e83bb0bddfeeaa635fbc1e41e1f5d0f231bc858e819bd553a54f026c44b6c64f865ec95745950501288f5e0a9c49c5daa5af82171d683dca07f37fdfcc509e56b8959782e9f29b4572aa502e4b7e34c063b328bb14b04c82e0d6ef46f30cc3633a9dff7f399630f16ed456977dd133b333db751e5bd93bcbf92bb3f8b051fcac23b4304a5fca54d426b38db42770012a654cbaeb16e4f8e3a69edd75c3e510285f3f83ebe3d9a85f8917db955d87e740f0ab256a263559004ad6ce4931431848c23342b99d0273e6eea814075ebe0d8327ea40386a6d9002415858577ec5f9c053cf72f9d33c851ce6605a06b2827d37bdac199271ec6aede6d4e162fa7d610feb9251223409166b8431c04d7e9888d1936502eed5b6f53693131853484fd364f00a10278df20a6c159542a2c0b7a075055fc1a50e2ea16340aa1da607d642e26cbe021ce2895c47b6cc5e47718425ee2f990dc6d862792b3d7a11c97f08ad2eef0f886a9cf370de5cdb5ca7df7f4930de78df2dcb9eb6eed7574831da93df8df46e98777fbdccc517b39f0cd5a995e103fdbd754f57b56518d91a9c74dd1152113a56c2c8430e6d465a7ef8ca4ea1bee5ba4d0579fca5bfa6b1c377bf2f907afc5cfa3c44ca9763a6ec76871a253776189785a8840885ca4f547a292bd28884a2d1f45a626fc5587f610a73f1d5b9fe3a0fefecef7f0afa7d37057f4dea5a4b19de190462eed6d4a4e0122067d3a3035215f9a93025c1d376d34946d7ec8f8fbd8cdef6c2a997a693845bb96dc75edaebb827e7d248b785bbfbbb4feb74488b99f48b5efa3ae824f1884e3dcf27ad12edf9445448e81f07348b21e38e195e54227e31b9650b4e7a9754ac6ab39a200fd5c9150595fb706916b8294a7ae39300d38cecfaed4c7fd9e263e791c761445677ec277bf3d99e1b0fbc7379ffd8ee707c2fbbe98b87d6c3bee2d9dc4d77621fe33f0a5f5a14e83be9ddc06bbdff243bd3bfa1be57229c83e563c528bacac496d633940179bb97513c2cdc6c9b9433c944f7d0dd391797aebeb142d842568280697027bbd513db467cc22097ca90522690b2a3f8d42baf0b8668dcb0db1d589298b0d80c3c8d107df02cfba1ad349e0a788bfbd4431ffa156891a794fe15ad8b195bd4f50934b3513ec86fdb349e748da6d1217631bfe9c79235dd3b465f46529d12edd2678171da6472cded2d7e29bdf5059272ce140243c5b1f2cd1887da321084e117d59eb02bc122567f17bab15ad9a9e455ceb28d182d9020a1b02b80e44d27ef6280fe53657aad2d936ea49be26cd98fbb1867d8bf067bff896168eacf9c02a2708b286d63dad9c29411262b40e19cac5a7b6aaab16ff43be5c8b7dde4a7d582b7744b593c938ce930a3500169fd8ca05932a0322c01a1f00d892078cd97994d01d83c173f1a19f275d00b630e7d0c1a12b3ea4efae4b7f09e078e9b35d0a718e6fe34d2c5d90d4027302380ecf86ee1d3e1f91393e3cbd5fba0b7eda67b2e5d3cbacd232fe793b1582dfcfc5ea1f14763fcb5f1724832dc069ad11e2514be0192cc52f38c974689e3c66f369f5d8d92bd26c75f3011e6f3fd1d174c0ff4f8cbe7b7b7f71ffb727fffdbe8ea097955e1b2cfc36d5913c9d4ebbf2c7c21447442e3f5144d780a25963fcd2b895ec54b3b2e37bdb9f0a16c7b9c5736eeec2f7e9089e15ff44c1f04579b584b1ee0724e308a82a85bc14a5fc956c66131bd7367b9bca9660435cbce8139abcf16fb79881454eca6e3fa2ef981632f7e84deaf5bba9b0ec273eb9693af5257a8abdc48c85d50154a14927a06e21826e4d54b52f5f30356fd13cb0458ffa5272eda4e5c3a0b8baaf33f631d8c00fb5116f39cc02be8d89820ae9612fcde29ac09c476e57c2f3ee56f652c7fcf6abae3158d794ec99b50399bbeacddc0e101d27e367db599de6f080685c7930c8c77d3bba1d6eb38f28f28d0deabfe4b910a58554d2eeb2a1c999793261f33571bced6b11b9aa5f1050dd303ac88182903b6a5c3ffc147f9f21b42e0842cc12a4f59f16f461dd19d91a10e5f3a2da21ca8562d308a58e48985b8e1c2214e74f46c7a270b5e14dfd63d0880eedb6177fd836881aa48e39f42bdb7868a16d107d113ea687452fa51701649db17937be8272ab76bddb5facb181cac99b5bcc9edebd18b7f47fd965b6e791243f8d551a26a79865603531e740854b031be3318490923c858b5da8bfa07a898b0c6cf618245d9697190121377c63497302a93e0ff6915dd403f188e919933e9e6d1c66bd991f9c118ac0a117fe24ca8b8b6a5c99ed5171713ab5693265c6f2be1f0996d23ac00b669ceca22882530a457b383d5073c13b016d293c56aa01be41a2c62d2af953530a200304267678048b5bd7516087ca196b61a21391f5fd0288429bbbbd3ff75777fff7b311efeda9df3be7bded79d1c10dae18cd0ced80776ef34fb935843b882d4fa3868a4afdef2e26eba467adf4d4fd14bf0c2487ecab1285e77883abfb7e3e56a6cc7125e27977ce3b864cb0795e64211ebfccb92c745cb6f076bd04169c75f7ad659fdf366a1349d7c6f26abea61789560e8101240a72c548d1469c2ef1088fe158d226fc4352ceb2edde4a5cbfdf91ba1ca7fbd4984cedf7bfbcb7f8181f68308d2e9c897f528d3a2c4405fe4ae9d55f671620c48b309c8ea2f6b69d73ddf1d9ab27a9b810f915aa7792c6c1306efb060356fbbabc7486f380f8c7548f195f6d39d093727180cfa1c2da25a8b8bbe232f708d374d2b5a4a46728c87a6414239f098dca2d9ddb36c60a1b3b4e7a7e7b5725a82c0c8860ba7c008241e1b943ce80d23755eda9ff46390c2945b372b5693ba0ffb964eeb0fc39fb2c0ee71cfa6f3eacc56fc6837bd13e2c83f4081be78eb0b3774cb7f8459a36ee4ebf0723176f8bc7688cd301109c0cfd18e260bca890cae6c44bf295cf9559ab68a38e39a1781682b81c881b845c4af160184038baf7e334b2c02a7c298cea92fb24c7a362aaa03050b7e3654ffdca76e56f2b4d286d5a0ba42834176c6e9d8045f889e7876740bf5d7a694926c7dd242b2d032da64d5659789a17a5cb65483211f7747d7c6893d80cb351baf7de9e358947800882e14503ef3fd0a491d633c0cd018c909508e509fd2195393920889646500f093144b0488c660c102530d8c691161f905d774f916be2c7ab358c23a8d7bc6bbe97dc204e07210bd82c17f23dcf018b9c02540bd024e7c62c4a54dd59eba6d25d42a31504c0ea68841577e61689025783436f2058ce5dc154afb2d63fde108724897716394e08bddd5a3ac292df24ee9568bb44c5c2e0656a834c3f2e06d77a353d6d5ba3894cb8259fef17cb6fc46e6a6e1b6b03be5e31f9fcdbe0c21dbf147a995ebb31956a167d30f37d2dde1bd64df99bea2d728c44edfdd8e7757a0ee18386ce19b30843cee1091e25839a41aed442a2aa479cbc9fe72df672b70c727c7db1eed89d58e4176645d3f79b6b61f7f391fcfa9df74d36fbd803e5dabd4c4d8c28b38842e8d0e668502210597264f1205179a48308427f204bc724df489d3e5fc37e0eba70a37b02ee13358d6c0f210e7e2d720e83f042396595159d9cab13c303b2c712a9bfc8c2d4fcd97931b2b83648fd139698d2405dc65c30f876ed61f223d78ec65f570a27835d63bfe07dbfffeeffffe2bd13cd7a5f3a13d3b5b281459313a103af857634222b497275545d27fe548775ce68e68e361612a0abe968e67ae7f828b432c608e339cb751302fd48ea05502535527579894b784e993d927e6d3f9db8d7657bbe93548e835dedc596fa730ac35e59117ef7a14fc784b5d5dd65fc978b49bdee979e4af5360e3e88bef55e7b9a4e51f09b5521d4dc7a363225078b173e478ffe3c1c95978c2c88a8817e2be41c96479bd8488b2c2ab0f4dc31f3fff615ae4d23ce0828a02298f1f19322b804d9ec92c9c1656de7599a9804bc914605eba955da1c9ea117917bdab852f91950d2f8a954b3ab2f83db4f78495f776b65e448fab77b58572ec396d2a9a78e74cfd605e6c221b5861be53b2aa9fef97ad22abba0f09ddbe96dec731f45acbb9082237df7cf33570c0f5b24158018ed2ef4c9821f6222742591931c75d8867fc1bb365b79172268659a3a265508631c98c4aca102fce3b3ddf9ffc6c91d0d99210dc3336d22bcf796ff16514fd37f0c6b7d32ede16012eebb29d6993616272892c5a0cd0261433ca302fdee04af96a8279fcbbdb6626e9755bdebea5d101b016717bbcc79abca64fadef2947fa80d3c509e5ebb698554893c26139eb10d6d6a665a689d532a6a67f4c6e36c1f882f7094f26af6191fb3f72abffcec5b16397b113ce6f3218e5db8cf36eaceff3f9738fc0f81d90e5710e2ce0a87aab3bdb9117cf6b9f37bbb95b5bff794a5c4c50261909c6c6b1d56304e511c7367c473abee3eaf8d69fb935c619526248cf7f632be1f3633e3e3acfc78f4eb383bbfef697339c53dff6209c65cf87316057e9bbf20a3fd9954ed66b29238d904a9a48a404ed6d68d68f4268384acfd049c244364c0bf56a529b2daf26fd753b3bb3976b8c8d8d75217588ec6f43f1eb11ccdbdc59727194456d769394d2d442fb2a4cfc8da899bf42ce76fd396179eec5494ba36f3c61f4b7bdac0cc054998b69eb066019e718e9e7f4e8cb15575cf635b0202b717bae93b6b942ca103d1436d9745938b0cc9ee1ee3c744188fc1c49112eb8aaaca9d272b541018e8627a2205a70f422ea33c33902a9b192704b1396f8306f18068626bd8dac7bf96d71a315d546eee8ce60cea64fae5a49cfd1d9f40d621d45d728d077d2d79e59584e7f570189c189b0389fab45e4d9929faee14b36ccd3b88e8dd017b2c4152617a9912f0a1b52288226c8941937e3c0a0e0f11ffb2665842abc4923b36a055bda1354e98b2fcbd072b36a50e0cada2984c6d3026db7f71bdb38ef372c7e9377c8e5f862f105cbe5fc5bd5b1d1ddb4b7f723fd4a73b944c193d2faa3efc7d6ea7947fb673b4b4fb019c3a2a8ed8da4de353b5dabf050b76ea6af16e2aab58bcecde7b37fc9c0661e748043db1ae930587518e23b2810dbdba8615e60842d06303f6a5c1e4c7a14ba8c1308130d17a3a5180fe9eece161f1946c249738864301f09c1bf93dbe06f82075f0b833e5633a76ab4baec45a7ded690d4ad24598b0c5b3be9b61b265610282c93f4b6106bb0e4015fe2a32f5a85a15a9f7e921806231d84fe8bad01179c0c9c9d546014d8d468615601de7a6fcd2ffc2e2d49927945a9eb8ba314219e096eb2e0bdaefb2f65027ccdd8904881d14ab4e2eb570c8aa228968e67a1cffe9697d3d81e752e7ddb91976e1c5e2a0f91ae5172569a443e524139b23280fca10277201dc26e74989e5f46ba782c3ca321329d5c0ba7b7f7a9afecba7e28695cef3d2b5b63b2f99ac631dc831dbee9a69b1e4f3f9e412715b7d0237440e848afc902dfbb5d4e1e327e785ada5140d60f90741287513be1ce2b694991c413cead2fa7afdc994dff1547b9ae10a43b64313bebc8ccfbf697cbe701fefb96f34ba471e015b1b8b368e0980cd46f6f96ea584a3ceaba4b5ff6326330792409dc872e68c55006e070ece59cbeedc5f6b078fa9bf6b7753a449557436e321c8324426381cc512fae5e0b57fc9d311c29ab8cc8a0a76a44a3300134af76cb2b3d38e183e8591b4355190ac7191c1a2595a7df9a43c0d2ea3bf863b8e3a131c158302665a0d7628a32cdb1b6dde30ee26837bd32b4d10f2ea6eab3f14274c3aca139f28e28309c6f1ef1c6dbd154f0247c0a7de4e1929d122ff579747a98b8891c90e171a50225a70d604efe0a5c2438a404846aaa264d2997f923f5946c5a2ef52a43c0aeda91aa454486860b39fcaa8db6a1e59b6759db6162c2d3f7c5d02aa557380eb9f6c52ffb7ad18b8b9d9dff8996f3355210596d5a655fb5b48cd11b773c698831afa9946be2146b8d04cefcc92748b1250a780939814dc7d1dead7af81a00db33052972d1eea8ffe55ffee50ba0d637399035c812cf5b14a128d47555e660134f1a5101fd3906ae9c0c6682158cf2edec76ad44c97756e6570c2c3d092f970cd0feaf0ca3d39866399f5f45ddcf85a75e8b7f23b8bf9deaaf75502d3718e7a2d1a529e2b3ee348844d9853e5804371c8d094353842e1533557ebd7ad23676b6ab9a64b2fc81b778dcdaa90becd615e60745de6293ba808ba56e79d231528291c296b241960b2e1b47205e9a9112e41b314f7ffa7e669b9733f97e37bf0f30f1b0d4545e8ed56e2ca19e56be650e3ade9691339cfde15177d3bb000a8d51be7b9aaf8f1e66a477c3bcfb076bb80452fa43028cbde397b1652c73142a71078cf1f4caa0625030b6e40b9cb147863450f863744f6061b6e32f453b766f79d0f7743fa7beb2de2b7bebb52f9cb6667e9e13e9d63f8ab1a51c848fed677a5add95cfedab0bd464749e2e180dc1262540c1c7d03529c8a8af29b34cf092195dc4a20079fbf59d9de9df08e0c6057adfcdb76f9844a63fdb17caa254bea235f05910bf6bbe58dcb52aba9aa9ba01a874f9434492a9b187eb4759763df662da21bbe91d6e55c5831c42af7db57483fcd135f63d2351046de9ad51a4f55dc21a150b01ad57a30720715917dfb4e26792d5f3bd6fe049583e10d2ba1ccec4f1c2f71471d083dbf235de02ba896f8dfe68fb1f13b8995fdcb6991b3e88d19031cbf652872effd4f153ab8125e968377d9d3e47b1ed14e8e79bcd7de4231f393b71e204af70cdf3607226ec297fe3c9aa32b8737d181d4fa1d1ad00226bf932af65498f3ea35820b32ba8414b9908ab3e3285f00455ea2a9cfd9ada9369883fc28a4ec11ba836e66880368e19912dab496965f114478d3f023466b10f63479488743f39919f4de39805f3e568f3ef1042dc36ddad5271c7404f7b94e1d216a99c26c4e60142785b92e318f4d538f383863a62beb68b2ee067e4fa22ab75fb33c271de15fae0073fa851fc7f40f6368c86424a2e84c20112b31199b432941d1ae128275cc28d34308b0a39c51d0971f00b5f3a691b97c117d35fc25cbc67b198edf0fbef96b3f977ed4ff67f861c8cf3e9ff02ea27da9aa077ad60985f9c9ed29291ae244132a15b47da29bca5a82a133fedb2cc6004b4cccc2af08e13875d084e2efcb72c82adfff8754747291050470df6997e05cefed9d6d6460d1077080bb27cafbd78b5d5842a933e4c261fc700fc3184e15bf8eaea1fb07138cc578bc5fc5837caf599ac90b0631a866b92a6e091bfe791178f0d2c8fd71b5eb43a5646faa70623fdaa7b6c21a5361e1ebd648fba1439862b2ccdd1e8e2e370438699906cd306542326c34e9a9b0a8d156ab01970d51791e004f6bf3590b1ec66b9e79246cea13a7966f59ac6f3e59cfaeffeeeef3e96be7c8be2a41156bba0ade1ca82e948832e3bb57439065c7c1303109d402404d4869334b5bb4b4018e5183a1a1498ffc77257ea8d3c7cf11c5206c7b864b765b1c8a9cb5751eee5009f4e5907819ff881fb79656228c8fc80eec1f0aecfce93cf57e7b3831b18f4c5d65d9c55f955e80d379cfbd7fc79ec06527d59f8133d577daef128858336525146d149901036c6770c04a21acdd1eb8e631010235d1d566343718bd653d2e4359908b303489e152b2b0927bdd2c2ffc0eba73ef28a4f48099cb8166f013a2e4a6ffcf0c0c65cdeef7a30a6239d36d9bd8a6f3f2c7888747d3175b49bdee97ae41f4e816eac3fed694ffbf46db7ddc683eab3ff30dce183a5f386237855bd5f3c1c69ab3940866f22206703807c284d112aaecaa2dba082294f84cdca04629b28d37ea0894c042e480929930da2c25863d928251dc1ce9e6acaa571d91f55790e65b47d9693f7ad1ec8d93019287b3687ea7c293d7a14d869b47ac1bed9617b0b6ef0dbb76cb458a90a98b438f584ddc35ffaca4682293f9d7dbc1609bd2dfa3d9c9267bddc7efbf6ecb4717bd6034f6537fb6f3ff0529fbb127c63e52720e5a342f44668492f4565844a6f83603ce42d0f7a2750c35030ee52337651de128a5de00c5e8cd1ec5653c8c19b2e4e2f60ac9df9e27f23e1df53faa751d02f9d2f66d789579c329db8bcd8a6ecce37460d0ad2aa5660d3122efa328d38d27efa1164c2985e919e979200143c15e26438ddea5af567a73c2b68715a63b9ecf40798b42012be4d8504824fe6cc0f7ad89f1218a008d32627cf30ed7209d34e5f43e79fbf9ceeff463f07b48741a1b1ce64965fab3a5e0cbd7142985c233d5f6eddf3793876016370ac8cf44fa5c4eeeef15ddee812a364ef21f5f0e8f80d2f7d07fdc85867a4b28b57e3ee30970282cb1dd768052e28cdf09f7ca20295ffe586149017ca9551b2fc520c8db6003bceedfd5a8c794efdb2cb1a60f3343cba1b1f7f31ed5cecaa73f0ed9fd0b913b5bb5af213232cbdf5e84bf559afa9e4343fe2979072204da11db4898c121fe844ba7ff59f1022413da2f5b6eb74f96a9e71bc813169afdcb16819eba2e7235fbfc9e2fc05b4e9566a4939a6b177f34d9c3f71e3865d7577c5f32bf8765ebdcd0fc8c960cc13eebbe4d94d17bebbb59dda1bae3fe7af6664b1fe6448c76d697a0db1d435a15825188c51e05d0ef936fc28ac6042cac78cc760b40b230f9356636d5eff055de3fd14ab9d71f21d75e782e04fa524814bdc2d91a09c012eea4c1163becd6a31fb1da0cb211d4b5ec9d8a3b3fdf587484dd748eff9dbfcb531ba7e65ac6f833d4a3ba2c0260590a17764312aaf2253e81f409007848694802b13616ff364e62647657fb772822a4fe068ebdfe2ff2e7b5d36882bb725131a06e2f6f8a0b582a4aaaf7a4cb1625d4d42a467c611d860eab0486c1e8b4f9737e7819c683857befe14b1f55530ba644dae8e2d970fa7b67f22f6e013a79544d60da7ca0494fcd02469ff3f7bef1f6c5976d5f7dd1faffb4d77cf78a441a361da64469618c90aa0c87154898d4ab189a1b0451ccab209c1d21f201bcb3176120a8c91906b92948db08248f90783edb2b04b4ccaa500896d04b190903cf680b0341228a5dfe3616809a9461a403f46ddd3ddefde9bcfe7bbf63ef7bcfbdeebee99e9f915ee7eef9eb3f7da6bafbdf6da6badb3cebefb9c2b236664868fc56c691066c5846f387bdfddd47bd9ea4ba7cdb7bef4670c94d7634e1ffddce7aee375883fc7843ce731137b94047880f41b10d3aba3784e6e944862351599040589702378ea09c323e84c8a4257a12c70f664a54a58346865259f52f00ae29d2848f1dee5e95f06f18fb222b79b1e0de4c5e73f318e78fc8117c54f3fd217121ad472e63f7d3756435c0eac5121ac37891b1e6d932b12edb9982458a0de7e3410ef70a51503a091ed42441875a111be0acffac825b0d69ff200d1784dfc22d1e41099006c9a643f60ff1ab8b7138bfc6978b893c0bc3d48651cb7f98ee72186a00e32594957c1bb927bf16a413a5083f49ed62be9a7b2927ea205e9d7b637bc1c16a4f740b007edf6f97b3171d379c1997236bd01d51139b106a5995f279cb91c020faba34b35d171b5d64381f9964efb2db5b5349927267c97b754e4574ad715eb387d0d23e7dc3cd1f3e26afa74b17c650daec6c2e504417001f29c017a2009404e911ba060947125f05b9b429791382624143a94d20e0a215965ed8ddcabf865e23bd1ff67a50907e6a45d5c76b18b8b1fc7a6fe1c88773935f3c9f22d3eb8bbb737bdb8d71e2405bf1e28ed0478321b2bca4f7c6087b5ef79ff0b4412181e58a96d0f90b2a2dd83fa4ef1093d13b47e7d6e68940563568eca2abe0cf9954fb3921a9d6cf3839923e7c599026c5357d3bb8e67fe42cd4a6b0bc709968cba9da90ee1b406384ffb50a532d7050a9ea5a7aff75debfadf508317fedf45e15cbc9e07bf11ef697ced661e8f5a4d3f6c6b5227e1b97f353e866df35b098c25b0a123776b1f890b8c9b516fe319b55c758fcfb71440e9b641b3d7fefa055e91fc88021d68c484cae5958d18640394b67619b304b7a29d344c0c568b90f62d2247dbf017fae4f5c3c2c28ae7324ad90f6daa59145c7ec2f3c134c40ffb038c86b83a36fd7e5a5f3fb4d35e29946fb047fd454e056730168b17723023a4b8936b61204ca65ff2b80ed6c7465f35973eee5f52ef73d7c47be9a697aa3d73e6ccd79e3a77ee1ef87c39ef4dfee94be13e5e75bee5858bdc4f26ac34787086551025ade314a61220488b5dc4d62bec3a37ee54dab41397bcc2cf8186498346a6e4c562b86040cbd5e5c26c53088d1e5e835973a9d2d93ae786173aa5f5f2e9277c83e95f688223b8ea2420945a57b4c946b180a972963db8ba6dff7589136678d14bd55e3a0ed14f3aa0ecb9560881a5583cd842c4f59f17354a3cd806e2dbf826e13b69fa3d74fa0e512bedb5676ac736e3458bf208c44d6ffd426b6fc6f9b020dd009dff2188c89e74827457d1afbdc44afa9311088e86f294cae62d75cc9ca96e40a305cc2d30f5cc3de9fe99e79cad54289ef35fab9168958538b8e09ec429df26bd4a7dff4b2b5e635899bdd2e310a5553e39417a75be7cad37d73179f49a9fa22e79e888195f1fbfb85a4e6caf0a9185ed14433dfdafd1345b0abe065522cd3755e495668884102d83e2592a93971098fe4b96d5ff0b0bfb5382f5dfc5d45f03e6df5aed2ddf3dc10236a4bcaf89abec1db0cb4a2d7b542f9ccc2bb973c33bd489b36fa59600fdad6fbdfd52a43bd9c7f5cc8b3fff482d6b2bb3f26391920b129123dda74a686ac24fe1340805f5d8a3fe2df21762e06ed9483b134dbe6591714d0b67355b7dafe9920e78ce31d94e4b93d1eb3666283083129bf06b90a6169fe32387e861df6afa5e6d5572357de3cba734df3cf48b775f6ddbacdf96b712d894407f289158e71efcfd17eaf704d451b498283cba6fc9b8c9c6da87e5f8a5826903fcdb840f0700b91694ae4b091096d9ea3de59637f4b1d91891cda5e79fd4bcde84941509cecbd6ac03cf281f7b0f4940156b147e30e6937be3c846f7c004c9bcf1a57e3c0c2287a5e77283f14ae9cb6b2dea42cdff10e52013110a08e4734342d9ea0a309b4c0249232a8c8316bfb55e64cca56e9f9fb5f9e6eabeb09e36b7be741bd7bb3ceae48f0a31887fc75b536e43ec1f397dfaf4838f9ad8a36cc8beab93fc3ae2bf60559007348b88777e2a9ff1ba7a901570849ced2699994c3bf2866b6646c75c09383065dfa68489a45dea0bee4c7a97d7662c67abd5a7bc67dcc915d50046b4d28442179172fed208c42a89088ed3e1d9a3a98ee186aca486bb5f7918a7dce159ef2ff7a14430ed03a83291074d6550865436c05e9f543e798ddc384bdf5427db992f850cb48ddb9e83b36223ca6af511e4faf768fc2dc8e587f961960377b8da5076dbfaf361aa2d0756f1c8d0677b1b852be917f9ca289b6a391ba013803fccaa6c10fb4afa3a40af55f41ea41ba04befb03de9c2b741ba521825f65338893a3e75c2bcc1465639540e92f31ca86a694eddad2a0a6521da8fe8ae9c10e2fe27ba26028e78a8f55698c9e49aaceb86cc84877a76081ae7ccd9bcef53af9a27f678f7dd77df4a8faf8c141c589e022ca3a8713a5ef49faa541b20a6a26c5803892f00e6854f4b89bd004f7097b21294003d8157d6a46f30df241c07633e2b5737c1c19ba7abc577ed9706f7c1bce668876734668bc53fe36d057bfdb31f0f5b6bdb60b41da36d7efdb4adccb728bd5d490c0c0faca6b700fddbbeedc95d4d774cf8e73f926f09f5e5cd5729c8f829c4a5d84a9e4838a2e4a032a65093961ba4cc8f0443951334a2cb01006c9e9762deeaa3ab8c65f4be6aa624ebeb38a553df62567b7103c6c7dade99e5781fab7d1fea417a8fd6350cbee81892f3d357d305b239bd2f400c0ffa76e4f1cd94b01eb0f7faed792b81cb49c0378aac16ab77a9c4fab2d852b3972c58a2e3b906e0ebf2403cb6101bf3da4f4afc917c955174cdc11336c5f54343307172f142ff681ff9d6d62614624de4edd67afbcd9be268a41f5d12c3d8676e02b07bedabea696f998686eee9776f7adf4efd2c2995c37d30f9cd741ebb2bdbc2a7fc6dbac90354d9022ca10c27073b8037b8e4d383f266f6f4591ea7cbad5a0895443822fee906def9e9e74d9e0e96377eef68b071e5fba8922be9ac46bc1d995def667a84fdbe4745e83136da5b5cb81369be4411356f5d7ba4159bc2f64fe52849d699a0c2bfc8958671e629a8249dcedaf9e7fa2d7e3e7694fb3a1a7af5a86e0d7ec34320ad6f95dc0a71f8e48e92256ed5505ea3b231884082572b73d4840f7b04370a63b5a5a614d4577b339d5ea18ae9cd8a63f1a770a555b8ad9e72bf8b3460c8031c5076f5dd51869edd02487fe66d537d9f65aeefe222f9b7b0cb5780fd1721e02be20ebc27d8e07be95616436d5f5569326227efc5892db909c22b482ff53e96209d87464701fafe20bdde68d70374f7a46f83f412ed2339a2856c7d718e0d1a9dd9288990904900af77d0d95a873e755d00a4bda72e70b33ae9d5f4c54bf6a9773715421c98ebf9c4a5c28d6d17bd7e7cee5b93c6b0c72b8f6dbe1ebe77bb7d308c287ace9147778f42941067c4e3d019ae20fecb179404b14f2ab34a2bad865cf42b80b38df655529680f6691bc191b444e169f63aacf7ef1274b74556ae2bfe7000c9a3f6d16d646767b9c7b7997c3c633bf3c5de7c51b6c5a42c26bbcbc57297bde9c4e9fc1fd89b1ea2ed6080fe547890f4b5af7dedcdc8e52685d1344fb1799d5eeb619f8ce6ffe2d3445098fc2be37a4b0519f4b79e3500e604a60d7027843d2b4a9ecb4252820b72d906e6dcb8a7256dac961f8957705f70daa733aafb7932799b417a79bde6fb6c8e712cd89bde6d642fefba675cdc34f14ee77dfb6845376dde4c09eb2b6de6b7692b81cb49c09bbaae335c6fdf5d1ea8292bba9d1829c6a59fa2ec4d2744d56bb41f35a744265a1fdb6a368173b35ebcd8570c8102b40ca853a975912d1fd93935b6000882ed6d6b306c8cd4ed4b9ed20a9afdcd7b960397b9c9e4d317e773e20e9f7a3b7205bd628c0bfd01fde537d3ee65daadbdfb6a6e995f7322a37cb4775238808fc827f805753c9df704f4ca21b0c9a7ace9370d2e3e1e4ce5c747f0f09872dbf9d26fc287391b215f719695f4e732807fcd88f8010fc70487cb657ef56a9348bff06e9e37f11e4df9cca7cefc2853fcad11a907850b2b25c3528e04e1997ccb2a8108e211a092cf04d04eb8c3c8589c18500c74a346ce65feecc38f745203b497cdac9318b60a7a726946d3ba6047d96c1c83b0a3c2af31a0b4a9b321fdc897cae3bf2be5e99f3a934d31295b1b944b26bc899f6adb6b59a9a09eb37947cf29f7cd1a28833560c8ca206d138848d5bae9f40188ff2c85bfc6ab90fe14d5af85ce2f7037dadead02ce28cd962d40cfd696d24137bff4171ea0d82d405f5c74bb8bfb5d7c3b0517ab4357d167b373e76a25dd20bdbdd9a5ef476f5b5db62be9a309b882ec7c3a5b74f7949564e79c76dd59ea90f77ded691d1f3547d52aedaa725c7a1aaeb24f7dbc7e40309928c57dea87c6e9a789df4f9e2cb70bed276a8fbaabe958c62bcbdecb9ee91e73d486b4851aa4b61463ecc28910845a4f5df3054a429bb13a7ea5c36da7c0c0af93edc0b6a96d04369b8c5952a98c4542fe2fe786fb67b08be755789e45a0fcc857503854c05ecf702c08cefd64430c3f117b9c1f024b90bedcad55dad1048c57d3000d40f2bf0facd4f22069a7ff649db939e72d42ed22ca9c183828b7595644900d42d287ea279d0b65262c19645a302a23474781fc4bb09179e618acfc89c67cd55b61e8c39b5727927de995e827c429d91f9dba725ec10c14e403da063309ea27d3f37cb3c9f5711da0efb8ed651979f77500004000494441547ccd3e46796373d16835dd6771d6176fba1dcf9117ef7e216f0c6e4f5b095c52025d67d41bacea9df9e6479d459f55f56c7941c763575ef39bcfd24efc33a0d5a638a48da6645d19446b47a9108052951887065e1f8ca5bcc2b8ab21ed6c1b23036e7f20d41226bdd0b7f696203a2463a094a5d0fb8f0fb877c76fe70733eb0636be0249a01ed4a1df13b0737b8611bf2b3592ac50b614be03acbc955453a0d68f2e297f0d647dc602bfb3c979fcea676bff6ef1d2396a249b4d971f6fb09c58ed5ffceeeffeeee24bd75eab3492fa9c59708e1e51ca2b1027d3ff07619e7654192f14a03eaca88f83f2fe868dcdb39d76bc47c44043fee4277ff3af133c7eafc512316787a8e645b23ad22666652c4cc5c0c97641db52b0cede5cbe5e01b12ede9202d3b9712224dbce02a334d501141a7dc4091ad5d240b113fcd2a79d3441655b0a00fb1037b70be0ab94293b08327eb9c3f8c050b6629920c47fad92d3261091a105aeec994ac12d34baca2449dc0ad0fd2a2afbd432861a4fc63b9b3c8069dd355b4cff090beadfc788be858e5ec1d8df84a4dedf5ffdd6080e2714f462ff1894c78050cb2ca463482ea4cf09d0e7aca2bb829efde85eb9b0a939db5c96f32b5b451fbfd9a5ef479789a31e1c7da202bf41104f9b4c1e3c0cb7a5364d1fb011f7a327f0c8212ac701dd0151b75b9a453e0dabb584289fe6ad16bf6ff3abfd2e1216878f1b2b125c0efeebf4a77bedfafc58fcc29acaa573f8afd7c3300f7ea37fa2c27c4626802199cff838e446b70dbaacae90b4617d42aaccc78eeb5c36accca8e503b470452649c1e4052917ca6a109875c2eac2b4ba8ded163f8305fd5715acabeb413beae0aa2c1fb7bbecf200690bd201cecf1ebd9ade03bfbe827314f1270ebe7851c913f1e907e3dc9bcfd419e2f3146d6494cc20c5cc9f57cd5a05e41c171c641a5240af5d48ab309c2ff89d0369909ccdfa2b3236ee94e3ff530037ff62dac8192bcd701a81f20ada495bc46851041aefb7853972f24ee8387f7ddb8bdf1a02ea2991c6f866ca8a3e47fddc91b7e7ad042e270175a6db36db5f3e4e14f169836303747d1386d43538faacaee7c6553b13435cfeb248181da7c752fb9ca88a1908ca0f2a359c1e7ef7c545fd6ae217191699c8460ec205361b3ad8937fd289ef94172af4c32e20f5bea68bd507d6df5a49aba7e1f21200e1466c6b6fb9f7dd50b8459f61d2bfe87dfbb6451912960e9217ab7829bea8d3fe33b63640514cf827e2a7332bdee55e763ebe59d8cfcfe51c78112c7bef7366b78f28edcca73f4920781b5cc92f0c3ab0c9f99327cf7dd08c17d9cda0fc7265db3d92c4b69b1fa7df1f294f5d2d4bf415ec26703578b58a430649d9421e9c50ea24a74945714f94824ee03a8c4bfc128f01aca809b26d92e639847e912efa43a7a0157fc0ad123d88a59a02ea8e933ee8338141ea554a9b08b71d0a603d79f9f07a52c652a17bae61b6b3a2b5073dc93bd35246aa004609454d0fdebfdac1c43de67700fa3e2812944fddcef25a6ce227b941790f9f4357cd6dd803735e4d396865dfe1d21793dcde920f5b5dd85b7bd10f89bde4282f67b64ae42b7c5706bd58f931b1887ece2746edc7972ff600bd6f751907e9dc89ee9d3d7b3602e98179d743db6fd34109a04bbe21a49c541ca23aa2beab20d889ba84440be2d10f499551d74659d54ddd44c3d83eb5e2419df90e375ef14e8bf6cac6be86709648dd60bdef53ffe2689f7a0fd0fb1cdac7e391b29a3ee3bde9da047f6eff89493838fe85c694e2c87b1515c8a364d4c4205e67d0a6ca6590219784d8a4086249d5a4a5b744271583365f057ee8653ec0e35c9e6175921beb3b082d7fc0965d96fddca87931eac11e6783f4e5b0326e90def0b2cda26fa778caaed42ea75f4b6c10d1957e79c9f7628d4c8077d93a7f5e6cb3b802ba2b854168dbf68a8084f8205f2ff53d6f5c502be3d6c50397fcc1e8fa6e309fd90b09fbb2b5c7640a0dba4eb5165268531e221d96f926594d4f838b13fc1f4b16ab8b06e9ec7ac90a9b01797b394fb0360f9b73b459bf2d6f25f048258037ba2bca8a72abbb7ea2bdcd275a32ce10c7805d9bd3fef2c15f362b12adfc22805c0120d403e1400cf0ad8b2f13559a896eea1a83dd494d7a26d9082b9c5d200d2d2bc38bed5a8c478bbdd9e4bdd45c2ac5bef498ac587f05d7a8bfaabf4d1ca49f706cfcd98f7dc6a7505616159b79a6423eb46ef38c23b8f64a65c666e4119cc9fd79cf9675b929f77cd1710ebe57c8797e5fa4ef9727d419ea36f7a73fea15f54f9d39f31aee86beb5af316422ed79325ddc78e31fcc6b69c617d97ee1dd3cdb6233781776b9f4b9cf7df4badffce46ff203422b5e8588d494225253586614a02b290a4d612bdefab3c2e4240051c09c6de1e4f40b6cc8044d4aa1c6d159a809c9a4d836b562570be1c22cd9675aa6dab6020bcfda6acb31c841ca3054a06a485508a4618036b73e0a255eeb29b424e515c2443e3d32a66c69e9c6611b65952487a0dab8e470c77435bf9337c1bd07396c04e54dcfab618e06e73c2495d57301b91c89d6ae4b5e7afafe73cf2a623ebed185e441f89c1574f2071e1605d76f8013609cca3697e3bc1d9d5f1a1d6d75099df6434606e996d5a7ae5396b7e93212687bd3d5ff596e060b7fd0501524be096bd7a4a82e3b31afcd18d8a878e6d128f0e3fba63b5f1d4a83fb29ba4c781630f3d69193a33d1855bdefd8fdc53ee0552cb042fd7a169276675c44ca3917efdaa923f21b3684325ca80a0090ca36626c07893866719141d9176d6267968156136e5ea45fb05a79a149dadb147f912a28db26f2346be3aa6b3dd074f66abe93faa7cb63c79e298c85f51e9cf7005d70f2e3205da0e97cfb15d22aadb753f415da7eeef54fea793af9ba72590887141f879ee6abf3c81649213bb288328273ca9adc22c5f5dc88455dc9b6daf572e6303d70707e256297a38f6f02ea617ce97ce981a8a19ab3e106b467d307b093f75833a47cc5e47d6b5f59ab3d2fb54dc937f0f86bb1071f22ed3753d2e973d3cf03ed6d662b812b9480bad3833f7ccebdda83feaf6224cf28b2310376a0afcf429e4e9d1abf718a9965652106580b97d452358417f5c5970611a3e024f5ea434a8113a4f4188ccaea37c60426fdf3420af0b038da963fa5143c6320a8e01808a41fc0eeefcfeedad0f5908b0e0b83fb03633de56ab97c1d2338e998334c18ed2e38cc07ec814fdc45930a45111d81c9a2380193cd388425969a9c31dbf8e0dced3dc0e1b0b1c032c037dff8d2e74b8492dd807a74c687475979fed13e4819cd44320056d8eb5b44605e642f77a1b59e2d34d7f4a0fe4a02acdffaadfb5ef0f0c327ee82e19747c2b2da944765c88f13c15405d625c0e1ae08d4ae84623841262f04b5980e4c0d0bbc4d101d59f4d2eddc79e8135bbaead451a1f6822b2d2f189e6d5981b5796135b1e6536ec75a31ab7eaa21f42010a3483b65ab72161d4ffd7e32c19195909485704359c5ce5080595df4ac17831459056ce12ccf01f34dc8c1805cd4713240f7236cc026384f9eb35b5a2a00afe0bcaf9ef76d10aaacabe79c129c73651a56d06977c100fd0201ba1f42084274dfea523f60b4b98a7e25fbd1bb6ec9ef361d9400ba7aac5611d1171438baac37403f0c4ad41575a9f9cf1010563a8436a9eb513c5b1ad4d2141b21a87f9efe891583b4397048903ebc7d24d5bc2dea09dba7ee6a3ae3626fbafc16eff1195c9de43f817b227558d37e3c6180daa4b6ad63eeb2b22c4ef6b5bb91827c40dec4488336beaf58ff60a537d165cfe9bce4954eba8f114d0af4e1c979e19cfee445fa537eab61b2fa973c3cfa222083df159b7468903e9f9fbdd85eee1b2403c0644687fe15eb08f4a4656fe73deefcc0d30b5d3d8b001cbb4e8d79e83316100289de8553848cbc85e7e28ea06a5ed455a18acff98d34030b764d53dae98ff3e97472e719b2692ffd6c07f0ced516ea4cfe4a7712e44f663f57c8078fbe9211c348455f4daf0748dbf3c2079bec833c95e6681f63dbc2d342021ffad0f8d986d947b42693d65126826d24a36e0be790f712e28f3cc7a7810c582bd20cdc65a05fab3deeda8134fd10a970022b79fbc8f5c2aad46b39d615ad6c6f039e6a90c597807154b21cca3ed39bb4b283c3bdb5977890b4daae667f88c6fce862f1205f1930990afb18a979c09ee52857411a2476734c69035c1ae48d7d15555d2fd28d874f56ae5ffb3656ab06b4c3f72ede3cd457a6df54592a496d201c5664006f21a8e5a92fb88441f71971ca9038ec1a788f03741f103bea237db72a747ccf470556be7e915f3c7d03ab0fbf8e805e1c4921b8082ac22b194660bd0ca2428cce295993759eaca310257402f8643f79a4ce44e476d2f1191257bdd82aa4c2925c829790850bf5c60fc0e4d3a97dab86ad6ffa70c2a52353e99673b1e6e43be91db7784cd78240725b416e73e1493cb7ef240208efe088e6a191a86c662a339cf7ac8b63c72687052eb4dec7839f04cf6d393c61b7b174c26fcefc2439db5a5c41b7d9661a827382f4052be6c4dad9da225e1aa0afbe66d1009e5d2efb56cf6b15c9f0bc027457d14fd50f8c12a05fbcb85a3d74ee52abe8dbad2e9bb3f108cbcbc9315727f1b00968fc764cfd16b47ee0a702c550565ff89bab7f20959da8b7b48aee8280aee2589f5b93bfe6a7ff4ae91eaa50d04bafa88bd37dc39acad5c91ddb99fd00db5776f3752e571aadd29125290ef3f8808ad523915c90acd7f6ea9b846a91726f1b99e00894537c87e7f233593000e685cd1ecb32f52e7591539eca5d7a265bc5c7d02615f6cd8532bc8ab35add04ed3b7766b3ef48833a1c19a44f26d71d7838912d667aad213dc5566a9f8f2aedaa4fcab35fac6556d9e5e6479921a4f8ce4c2107cf0839d7828c0c1cc5a58e4b2a6d7889a8926cb256d0b918eb67991fe70454920d39a10b21efe4054a3df3ea652208a14959fa2cdd712d799b3526b7017243b5f69d75fd5e97c1f1ada5ba40f14dcc4b77be298fb7bd3cc5e628fc6d0f4f1f097ccdd7acf7a9f38cd8c7f428fa1935de85ca78438218555b9d57a94baf556e7102c5c6b0158d32feadf94a8a3a94c457da07f8fab37232b6858436265dfbb35ffd5b55b4b204ecdf00bd629dee136d9836696eff937f6f9645078edda47a806c4d526c09fbfe2119d1c756d2861b3fc9908f3f283bb623380cefb9f1b05dec3d4c2b0a44000d680a36360ce9e5e45ee917378da7ce9a152db0bad0de4053a0fd475f9fd9218f78459dd5f4ef84f3172bfc50899732277b70cd0787f4c7c86465ec99cf7ca6ef499e1df519bfe5611ca46f5e9c3ff9c9fb5fb1b8b0f791c972c1de4c7e94c40ee8b35f00538a840a2e2b71ba1433ddd4c96a6fe744270fbcb8afc6ea8a1f93f5759166b2003a19bd2e081472e105ee455b9a362a2550019d3488e52c4e231c59152d95b968dab1cd2d57bbd0b61cc5691775e997da045f3ee4bf14c4d682a5e507bee9d3585ed652a64e361c8bed327d94f922f9577da7793ee226603768f7b3e75ed6bc5a31dadea07dd5dc33179804e69ea91e52e12c86e09cfa9058f2152f75593d9fcd56ec4167059d748ad57313ef8d630dbd56d12dbb0fbdef453f6c15bddfdc8d75c876db74451238a19e972eaa3dea914aa3d6e8b2d565e950870de50121ea68d2f26285004dc40db28eeb0ff46ffc98ebe639ebab7ebff243a752d8ddad074a7d9ffa17bff8c5f9eeee6febf38600bdcfadb0ab95def7bef7ddbc584ebe4b56f3c0ac23f6a29371d30b158e254edb2b58b35dc72cacfc8279ed53a872f36cf2db870c017971d60ea5eb51524a8b4cb07b1f14bc78098cdfd0a7f8279270fef3401508b54fb402ce22b2daa5fe7664fca37c8efa3655c67cd0baecafbd375d580f00c71703e14f8db4f0db820c53978618923ce7137951182278e56e4d34b8e46d702e1e72b4367e55f9eb67957f6878ae1bd416b1d056ada68d5368433e9e9c8f5a31272f6da090216b2da5eafe83dcc07e7670a3b63b22b9ed85adaa06e6e36f37e247b19143de6fffd632a223e86dc15b095c8904facdde747afce3069bda9749bdf706380f6c0274f172806b5b714a9e9b7f02563e5f7b2a227a37df98e40d80794d2267eab515402403f0667300736d119c34b448db98b70cda4e9e42d3fe9793c57cf52b36f166f8528918e7afd023b16b9169acd2a47896317d4252faa00639d42256f5cd08e1daa3e3910b4741128f925ce30dce2f56ab4ff14bd1c78eb12a99e47378f9066db80f2ff811c7cf6c2ea983d7e74b2f73c9e4be7046f2c35945089ba22b6c1c1ad160bee2cd4097df61006e702ec6b1635f9c7fd10f1761cf96f93a3c2b6ae218ccf780bd07e85e9cfd95d133f79f79fd9933f7ff26bdfc34eff8e6295d492a28fe32979439474896e52b92a3c0bf4eb97ee4c766b441b2a2156a5d30d3a6201cfd13218d6b126c6ab12b1965272f2a26cdb0e444a625709a7325cf9d66d7cac825e2a01654fe4ad97abef124efb419944862624bbeb26802fd849f52940adca19d5f02a10a448373fb502e19b76d9368e338c8e7ae160b08dde5e457868b4a2ee51e2a40e7ed2e1bc1f762afff389181790fce3bd29c6b0b8d1fa6dcb6b6d47b9c0dce975c8c0cd08f2df8abd8bc3477b4827eb96d2e473d309ae16d0f8f580238272efcb81754480d2d2d8d5644cfa2a7da118ed7f7eb8b9000072d8a064617014a80665961887d4cafe7c58f37f230292eb4de4087ae1ce24e4fe681d235e3a7d75972dd27ec033ec602f1cfebb99010e0367b88bd331e6d4f019481301cca8c29454bd4670109406ebc29e7975ad3a46105a7d91870b7ca64e51719366b1becdb725a81a3fb08cdf811e52f3f1169f2a96ffcf8f5b3be2da84192ceea5b7999d2cff08cc9f3687048baee00ecb0d5f4a7d6b68ae98b1c648d53e5432efe2397b84aa5e7f8d14d8ecc9d72e3939b2ecab64dd94ae0a28a6f917373a5699b881c982da4e9392a0d8dd04e1bfac9950b5b002aa97c787da3585e0b739e4fdfa6a2e7a722cab176f76800a37f4cd9bc7756ac288e837420fb53bf9912fad4bca1dacfefb6f4f490c08778bde74b5ffad22f115f9c4171fdc7b08c6c1239680928a77e9d9cf18500828b2cec183b00179445067215e802911075da466e8c5d3137c53e5a5c626552d1305bbd92a1aab72dfad6c6328ba71469379ddec73b193f9f1f518c9d8d57d20793737fdf4be0f935f2ad7df60ecc493f8b2364f4a2e1d4fec5133f1ffa4ebd70328a22ab9db4480cd528893499fc36b6ef0bdd6b6bba6cf8cee998fcfa1e9b9f753cdae63f03fa288d7db297a74ba6b30f9f7a1decdd242b32eba02ad55d51ad54389fd357ec4ca77fc6ba6304e793df9e4cbe82bc2b66bb0fedc6cdf12b84ee5d2578df1fb0b36afa75be6e91f7b3ff1c4eec37a6f3e5ff82a2dc624f25e4088252f51d5e8c4af9ef7743ae3855b00e8f342a4183198f1a3d0bf7d9942458c1fbf15435552e40ebcafe0a537a01aa7cd00c27514a1430a44af1bc10445973b1a0ad9308afa248c38b8d9f5cfc8119fc58b60d37266448d64b3b7ca998de14b5aaf459edc4a9ab4ab549bf66350c69d8c43e1384b4c02103a16635fd045b5a3e2b8a2be8f52e52b6ba6c04e8d6b275818bcc71bfa21d02f4353cdfde129cb7c09c68c86d2dae14f5e07cc51b5cdcda62906e6c7e8a0574037370d8e2b2ca1697a3f6a1035f8eb7b9f495d6f12a7a87c9d3365d9904d0bfe32a24ee37fa67ce7c9c2acaa379c59101ad9b52e9aa6fea5305263d3857d9d2be29dd7cbaf3dcf82896d6c76b097b6c82ba32eeaefefbd4dffffef7df0aefdfd538d5481acfda178362c09e1c60f64687d11a6760d854f6a92b006c310f7667bc9495234212bbdfc807161f25a4cbb8e4276edd4c373f224dfff15b9a743c0165659ad47c49f117493730fde90f56d317b229e967d94bff0dd5607c1cef4e1fc3d7f9f15ec835f4c9cb2d17abff34ba862c7271e4a42e2210e68831f3a99971ec8091cff06d46c3adc0dcad5a717e404b96f1cfe4a3ed565913c13afff591b6edfdf8271b75e0cc7586d90c3c14c571d266d32f70c3f5ee8acff7df978ec30849e147f7fa2e55f2dd44865f22dd7c88b45fb0fbea9a34b6692b8147230175e96b78a8b4b5fd581c3dfa9c384447af525740829fd7d6b48f580079f5de727ddc9e920602b00a03a1842462815bf10c7980d92a0d5ebfa65867ab76a131175b7697801617bb83867f1a5fb751fba3ddafa5410ec3fd2fa58b3c485a5bcda6d3bdebb0d337322eef89e327a5237f8d4628c9432c1cdecdb7918a14b8dfb8c6834410b6c73b08001e62810b98dc9f184a764caca4d72d83d6dfc54d36dc8830a4de6232d958511fdb7b7a189a6c64f2cef4d5e2af6604f2d587e1c430a2ec65cda422bce9e42413fb669ccc0f9e3d71e2f95fbee69a6b1f6a013a423e71f6ec89e79fbae69a6f2266fbf3a74e3cf43afcd34f2c2eeefd02fb233ecdd0dfc744fe08782fa78fddec7f8d878e08e98afee8a0bae51cfde002d884ee293046931563cafe3267d8756632f97d70d2490be0d22d346bedc70fa0289dedc4ac7ea8a1905ae146e7695bb43c0a2bd5954e20e9c37c026f09032e960a96081cc2d28dc38782a4d33a036650164ce1a7f104d056b95007a1f83600f06e2fb4c497c7cc51230a83dec8007a4fddfe417750952193ee3c106cfb232a1b017a7f28b482731f884a70be3cce1b5b8e71ed39b6609e139cd7d6165eb148846e906e703e5e3db78fcd2d2ec27a803e0ed2853f1e2badd2fdbd9688b54fa9cfaa968163254bd815ab2815a8609918968166ec4f2715fc689e46a632858615458bcc8c1fe8690fcd49f7380b0ae851a217ce4706ebfd1b36db98aee65cf3039d6fc0e5ee5630c61863cc708c9fc89838e462d0cf0e0d9ce6229aad176eb5c96803af81c3f0c8470d30a9db577ae144360b099473e14be7d66ab36b5caddbffc16f80512bb7f2259d46531f91f9e117a2678b37cf6793bcc211e091099bec139e5fb87caaadd632b2af29b1e84d9bf8786e56971eb8a2c9f09561c1d44845e10c455faba5222471640e945f01cc4b43dcd68eaa4c1365ff45c80d9c7041b4f59c1b82ca50b05771b181e5ea9de8cf4107ca757a51abe9622679ad7621835d6047afae355c4f4fb5f919b1b6cd3ecd2430d625a2957ba3ee9a470228ec8dffa9af3655e731b2f2f80cd2327fc2637a1c63178d80714882fdd4520762f0cbf46c19496963da4c3ee63b20d501505b7655f69866d5b6d1a615fbd335352f29bee1c560b8c2e2207258ac667f93bad38ea178b3c65e814238e3a0bbf8d906b7be2f886670f617be1af7e4eb41f51009ad04ed34e206e34cadf00376cb0bfcd4fbd445cb8afa41df6015c998c91f3bbaf64bf563473d40ef37e8e2ecbff517324ab39dd977c3c349bda29320bb7e3248c62ca44652c305c23ef2d50f31c81f221070d0bfe364d2fc869c3904d37326127ae0314c824beba0489d3917a32294d645da2128bfa4a86994d1c20d7fa89413527b3a812b60680787c035f20ee5ca0500be1492b78155ad68a6f799896dcd825483497f692fffc14f87810f635129f257fc89e790621132a81c30128373efde52b69a7672908170ea8a6595e3543e45a3c94d381f59f3f256c19790de5871d498342268bc77b2c375c2db40344c45d85c4d77257db1585b4005edf9e972b0c5afaf6e89ce17be6fe202dbce4f71f3d876b42438172f7bcfcd90fa7e59f306e89edd7feed9d403f42aed5f59ddaea277a95c85335fd30c816214a7544d052add456b75dad15132aa76ee584b83f3db03cc9a8e362da25385c37bf36f9d4e759cb3b828dfe37374787ef858c6737d38c69543dffb81f77e3ddbf4be5d5ebdf410b4c7a01c526cc50b533330cd2a38dab463c236b5370e8cd3e4681db736db2006fbf131d0172f760c128862b84a64419b94838e1b04e961cc75c152dadeec73b64bdb4220fd937742ec5d4e1244c66950b693f4057c35f91fa0f587e8f97b68f7a0cd76772fec9c3f5f0f29f22cd1dc15db2f7ff9cbb3cdad15e23ed9e9f6db6f7f062f2b3c1ddd64f08eb652cf21ab72b380d7f2f7c13435337aca3c47281433e7cd5f8b9dacf2eb73542452514178f563bb7c1352824d571db57592fe84297fdafacbcd94864b6a2207bf595a14bf29670b8c7e1716b39ade9eab3ebe77f443a4101df6aa9adfa6ad041e8b047a20c86b473fa61e6a39515d88aaf7faa96e422abefe27fbd9319e5a3810461b3ee57bb4027d16be12bbcaaa383aefc2a5f6161a5a4b906d438f54f4482ed718fd6c30252b6dfd9a6dd24d78b09e86e7d902784f5999b1efc1201d7e7985f8f4e5b404dff1147fd212e6495fa15b6e5d551c0abfba855c13acc403789de39f6cf96e1b48a7b7ab402cbee2a37bd8f54e6e1cfc9d183963857ffc3039644c04e65962c7fee3130abaffb8f92de7e0f2f6a3550986fe82038bac65ce9cff0c3c0305cd4974481975f2e203f10a339dde00e8064735e0a5d659f4df433212ad0fa7288e972c3bb69a3327bd2138b4155ebd549de09a05ba2d7e2348792065055902d20aef6d4c2a15e0c1f15b179c46dd4901900bb27d40205f87ca73facb4899c354160fa9b39d7de716844e4b5ad2928d5c4cea109a8e3718b9f042daa641949934b75552642f5f96148cff7c946f58025f634a431a5bd7c797a0031867df8ff7c128bbb781fc1f1ea4cf07455a24685f3f14eade73ef04e9f75cdb761ee5eb41ba2be7fd6370de3f7df5dcb3017a7f48d4007d731fba5b5afaaaead50cdc14ddeff9349b9e3230d109a91feab549cd591b01ce53dd42bfcb79a3ef9651b429abe9b6cbf32338f634cb298717e8a65c4567ffd33157d4c769fd40e9ae5e36dbe3ea81d27abee56acff57c35fbb1b2308eda030759ce85a0d95e9c716c870ac6e70dbf8b0d915198b721ffddd65de1d5be9418f626adc8a29a03a35133c85a8d05973a539e65a1650241fbf41b2ee0dab02e407aa63263e45c5d379ace85fdf2673be70ff4ee5fab6ef232eea17f0ed87fb6589c3c3669bb5f0812876f333657d5edefad6fbd7d63a6843eb1898bd7f31ca863ca754051387ece19b3e7e403ce25413965b55c41c5af96c0c4f7df41a78abc73e41fd9a1ad19cbd2a9afb629b506ea8ad835edee49a74afcb4a9761c3f319b2def1f05e960b5140d6f79a4bbd31fac6f8b1c89d3e7b51872d843a4b61cafac354adbd356028f4902ea14faf6318da9fcbf9613ed4e4ce47281c5c453e0e871e28f38eba32cdb966cf34f5e2bc4d1763d172d898863ca8267a84bd8e63a44f2b409ed20a62791d3c6c39a52f2f7d1c7b9dc0f13c0103f80a19195a14d178b5b20fd37245594ec1f0a3a78fc651e984d59c28d33ab2dc28e63d0d64d8e8eb7e2c5d64b0285977194108a060d817d70d8999087482130b6fd506421b29d2f771a7ff3216ebb741c6cc62b11bf1981dc169ec1aa8b490d4ca6cdb58b429b94a251632f8cd433881a37ee2ccd71bf4ea6ed392407dc89129e20c0ab0f33995e9c7cf2a1213c330a6e5a363ef2153cf4c115af7a973ed41168a06d0cbc6d24d48a95ea38f8322383e1a34dae7da5ecd80cea2bb06f14d39735fd5e30638086ca1a4d8cb2d536021537177209667c455b21e4db04799579f8ad15382964d47641a1c61786241f7ac25320d378a64941e0438219832334af24ec6ff23e5776eabb18b43d113b245aaa95f37590eecafabcef413740e76f75ae02f4dec6730fcc35a41e987b3e2c38ef017a0fcec75b5cfa9ef371c0d661e3feb6f9c724815df5241e804ce96ee9876075b5970c5580c4d1a94309eab1a3282cbadcf7aa177ee0b74a639df253e93b9b01bbf5048c1bdab76e7535721ff8c007be03bff0122da9fb0c3db2bc1a8cc7e6605927aeff70a854732e7b5130fd26c63659250a63cdbe68c04ee808d007d8b37ad39af61e6390f828edba68e90bec948ffdc90b9ffc672234f72aeb938249d939d18ea1e23490e49f0c34cad6a923af3f027c0ba5ff93c0f7558b93278fb9aa6e0b837557d5cd637b3baeac9b377ddbb7dd7e8115ed273b587f41c6881cfcd6c2805855abf13262c71a29447ac9c5ef479e8a4264f09a5c2210a0ca5d9165b089dc2928e27e5516273a000e9d717357b494b1c221a56f0bd09727852c90d35d1b4e345f7efbd537bfb1ac9bcd82c76c8f3cfb5e788e3bdf20f6dffdda7cdc77fc4dc7e6ca1ab4b6692b81c72481ae53f8818fabcba6bc2843a5360ec1f67ad00d20fa1e2f6435b83d7e2ba30468b8961a89692f9cf5639c3d990be9e0d83e35a9e70099f8abd6d2d67c9a9da5894430b2c096ab5f8e23d3aaf8b0584826e6256467359fbe111b3f9971b4fec3418b11c1497fcd4786ae7df8ccac1de481fd749a5652600c7264a2dec19883e781c7c9ea016e7a3e2b5fde375ce46b346ddfe328c9df647a85af66ec73d4dbc76ff5c2f88c83ff4b966554063dc95cb80bd7324aa6fe8b69d114a8b83623191657e06a5e25a0cbb41799ff8cdc0b0ec856a5b119600a4e2405e4c726b980aa58e4ed244a222f21169a1e826b5bf0f8f10c48db416ac8356512e44734037ac9155ac62a4fd2a971f7c902c90639369841348c142945da1a26e71d28755e74c4a113576d1ca63c879a17083235f98e9b3c1fb7fe04472430d3062a911165fbeb01454908fe832b0f45c33695aa2f09c305af65049a8f6f7959bfe1a51e1ced6d588d6205a83f28ea3e74837457d0d718eb00bd07e7d65d2a383f6af57cbb823e96eae39c5fe1d4d429bb69c14af22a554543e89e7a1d2d13587fe88f3a956f76d441969d83d1db50861c0b858bdf2fe9715269f646abbad66d062963fcfe6dca18f648f2bff11beff297647e38464c66fdcd40d9466831c6b2244651ffb97116aaed0bb3dee1c5288599edb2b13d5b6712f005a9d507c7e6080bfbd68e133c52d41324ef396b0f350f4a383e35d1761c1b9035bdecab07ac2f69861e9e9c0fa7d040d31e7d4b4ff98a155b11a73fc2bbfede74f6ecfc54bf291a07eb8d7c56d3bd3810a8c7b69fbc807df9554aa8df944482913539e620df8846ced13364912123e504cc4adb9b91d2cfc8a5cfa3f2f706873313eb75c83fdbe7362991046505e99c7292666d8771163393d4d75ef99274758e3f7f5797a3e7c40e5ea3eb89ea5479934a6084c775873ad6c19dd3e4ecc9c95956d3dd96d457d353393af46d0a23d036bb95c06392405fadfdcc673ef329d4faac7ec76f48633cf12fda440c207ea62fda681706b855a55d81dccba26b141ce2c344f2833df90794aab21a992fff57f898644b585948da87f66a9bf297e9973cb47fddb0a556d4c7bf407a51bfca76bfc90bc3539cb7ddcb87fd49cbe7ad283b46eac30e7c55ffc001e47a167ef40a24c5d2683854694306f71baf21353ff5e34be4d766efcd839f7da112e52b4b9b761f5e369bfa8a441cd69f944158af6b0283903979cdc014441b90cc6675aa09d9b2295f7f88e750c0ad760cd44a6644daf95a98b3792baa4f4ad0323fdcbd390dd2b20f93f41220871ac550a04d95c374e71765a9b70ca583f0d1f1bb32a84c06e90ac479f1676c8b270af229593ee285c7e453c541b6c0cbbfad3a751b05ec60ea235211ca29dae2585b1b6b7bfb8c1f3e72ed006a5d30332f8527aeff06e8de1507d76efdc8b4b8a59136003ce54739a6ff4645cf9d690ed26de942bdddc55256d6ebd74491477ee67a5f90ee0a3ab487d5f32b0dce3757cf7b40b65d41ef93f0f89fb1137e4e392aa1f226a78645654a6d067d0b563c05f5514274ca33c18e6dca66291208e5e6924aecfab9521d27be9589b6a15759d1b56ef39d24e3074a1febb7285ffad233fe1ac1eb2d15d785f10ad6bd7808cc18ca99675c94332eaab4e2d85606106322673be12532dbd44d731a443645d2a376e7893ce7047c14eb41d24e8f5ee405b882af20d43eb8a0e406a8da3a27dab0361e7f057ab6db4926b0eacb623a859d5c90c2af08936fa7ff5fe0adb8b76e06eb9467aeac4f26fff164f2e10f4f7a806ec0def3927da212baf41c19ceeb75d369f9b496b58a7fc7c427fa5763ad0b728d5aa128d304dd4822f72f9ccb2bdb5a19eb2fedc7bc922b7d48366de9172d0d3678d4a64d6e82c476de6c329b9e61aaee0794b46f0dcd75b500fc92a2bea8601e798874b9c896979367b951dd7fab8a4f8d35f660aa91dd9eb612b8aa1250bf120caea61fcfe21ef6a01568574666fed5b7505a4b0bbacaa1955f139b7fac20be497bb354c90aed8a0f516d28a64a5ae9053471e8457840f92e0cc3a200b02cb8a88992cf747516dba92dbbc00c83b3848d9dad16b36f241e7c95c41277a63f4ab0ee6e106d3dbf6f6120485ebcf04ece7de8b5075f82c59f63ce824a865e63b456ff20a7fa8cfed61b7acc5b68bc81084f9e7ba6d9bd6d2fced6cfe2591ea78d17be8cab92d7ff1c96fe025ceeae576e1568bbc350b8b4a8e13018c6dc03410345532edcc015506838610e4cdcf4d8f1a4455e5ccfce9a55c9da56d05a4802caa9aa00c0418fd085f397bb2e27211e548a12902341945a9be0d9a970272274a4218f36202f7e2d4f67ca3adfb6ed1365fbf41112ad234fe127339cfebb3c8a3247eaa396f20a05af02b9c34c1378084314f8f7c643f97a8137a52c86839757ffedcfa2f8c0f93551f8576ec068e3c783e87ceee19dcb9faf5ff4a28bd16abafbd027c7d777807cab929b572f2cee47e7a29ecabec5c5157449f700bdef39efdb5ac62be79bc1f978f55c1adbf4c44a0095db552fbbb9a02eaa127a4666d035f4253aaabe51695e1da5205ad954d3bd56ad5a12b858fa8f3c6ea6cd7d15d75db73f50d9c4ef37719bf0cb95f971a367b1d2f95afd826c67cfb9815d6cc04351c8c5a4db9fcc93822f8c41e683815ae3277e00ff52432c6868464efa2460d549e4a8579160b76a851cdb8d710a97b07cc929257c8e55f1a969daf8a7cedeac93895c80a0abab73152234c9da17a5f884229e065e885ec8af84bcf3f87cfe4d9bc1bacdb20de6d65b33ac1ea03f19c1fa74b1fa6a7daa53a11839326665036bcc47188c82a97722208b421ccacac979cbf82d4459150d729686facd277e5c5a913f78f4a23ca3f391b172b447e17ce4894fdd44b5b9984cfe8d2d8794b7b2655d4dbcbd8b53aeddf854fc6d7ce58097ccc97d0fd70b1a6f7bd95c55dbdf765bda4ae02a4860baba4f0d8fbfc12cb4b0b82f618dbc306db26f1f8bcf4ca5c6d5ec456c8b18485c5b0fd09b4f4d75ab0f1ea892484ca67db6f696bb6d0e6de21045991910c79034a6dc037358ededf98346ffb30cc476210117f183fa083da73b26749a5abdb0f297c0084afbd663f90a3f4d00d290c7141957cae0e85fe328149c38acf2efb405cff5b769d6991232c5f6ebbbb482b6e3e0133ed300dde6fd76738c99aec600f3f0f41ab90b9b7aa696b26fd20b3549a63dca6826c7c16700c24d35c8fabab97032c1b45748bd75e5c005a0182344fb34e8142b4da1ace3b491071d2d5169f52717c507539009719f5156e25180e88907f9a66ded4bed2dda64da1e929296528bcfcb690726b4da9029de444ab306a78fdc00084ba794198759275afa16064a8ca76e84a86f1d3a5ed11c7e0fee43c07654482bfc4b2500792d090288ec6a8cc56308854f094a99d364f276b7bc74c50ab01dd40c5fc568b16f81392c48b7de207d33403730bfd2e0fcb0d5f3c7ba822a5fdb74651240474f97e6d4514ba8555a0d45fdca7fd902c14cb36e544ab75dc14b60f1f271efd1677f08482d461b6f921316cf77d83e159f3ae60cbd8a2322683c50d7f1c63ad261577a3e71e2d81bb0b0eb13d43a181336037b9cb5153ff25aa6d1b742685fd99a661b0d0e84046f09e8b4bfee325b5da355f6aaff02a7bac038954bc95138ffca05b2f0612136499e0b48f90e3aa341ffb54be5585f2fc3a7f8b696a0cbba948bf7367fc0e277e457cc7676261c8f74f976fb7a5adeb9339ffee03a58ff62f6acbb6f1dd8ecd627395827207e4e065b03469e0e047feec5dcac82446ef946c4b1712dc87819b323cfcd2383753e2273cf556363318239f3b902e75f9af49500de9ac88d326881794e130ee0294a8bb29755fcf9e4974290c3618aecbac771fe6adb0b73c0a207bf2d92ab37b6115f2bcc37f16c57d3bb24b7e7c75b02eb4070fae172166547ae2e6b3b65081803da9eb7b8702e7715af54ece9c7008a55bea7dba2edeb133b129b62dd1aafdbd8500f6cfc13da690330047b1b8ab1b9f4738fa4c68967406e83ca8fc18abfd41c3ee0b4d1b30db62c490e90818faae38734c5c2b6399ad7a0edd7a49ddb69dad1441cc1e281c6bf81575d0b56abb3abc5eafe7e1bae41fb61d7dbde4edee93e8ab7cfaf4375164163fb2e80b248e22fb07be14ddadc9f2e30bc57751df9d1a1ff1ace4ec781014ac0eb60e1ae5d1f840659214820c370b99c0139126b73c762d75e849a706a753d284c4e13606f4f07b6cbdd0a9dd785b409489c46a390802348830b672471ae4e376c159db4849d0a86c1933dfebc2846b9e8aff32fdf990068c469530efff6e085803e94876906df190ff4206553927c549d7882ec5f76d2367d179e50952206216ee32b8d6c61e36a59b4e5cd1b0e2b18686e966c271a78b9b1490e0c3b845f272bfb66450296875333c6d5595679ee069adbd2e1764e0069bcf3bcef4b2ff8b8661da45b375e3d77d5fc522be73df0ea67db6fd3132f017e1f81bddbcbeb51a6a8ae1c442f637da5831590ab5f56eae4d45bf2961b766c244aa77e028dde696520cda6a7c5345d684f911ab417643271f3784f3ee8e80fa35dad37bf7cf8d73ffcb5bcf2f0953120ed17be8cb36b4508de32a6663bd845167404fb01d1723946da0508a70e3e79e91509f94f20a93ca856466584caaa88c40fd0a0e46b03db78abd3e42459fa94c77461cff97ad616fc79c1887f012713e0192291386dc39698926ebd0013209af5b6f71590fa2b01f0f6bd50fd196eaa6fbc70e1e4cecc1fa823f560fde69b6fce3cb9b2de3e092ccd8bf778267cd52d194ce6cd61c029b2717c752898f2b25c378de2f09f53832b17db72ac4b74c115a12be2cacfb9cb03a4f199a2225b263ff341dbf2adbdac0e91b75fce6e6542929fe6c5b49fa065db976ea85ee1baabe9c2fd11813d5e886996eb4e1e22357fb9e4cada3a98ba1cf6b67e2b81472681be723b5d2e7f2b0fc863177a0fefe63952c046e23b300b7d460a02b0818e9bfa2a17322d3130fdcee0a22c4b1180969873f29202de71433404ed0134a33b2a49c69d59f45cad7e355b76f14e3b137ed8686f7a23183f0aab27bd94ad796cfd405f8ab96997960e2267ebcd62c39ccb9f7216858a6cd7911f3ef2921b15f18df7c4006ed13812831e7e7cc9c0bcac9fb82899b84d307b3a8f9fa8573376c861e7c36c5fcef7252e067f32a38069399755456692c11a8a274765d0abdf82f1f83eeb49991cf12dac2f2c55db8ed4e5cead35a9685bea26e8d957ba55707241b24fc19ce5caffb4b0d28b1b81ac598fa2161d333a554a81514726efdd956f3fceb229f438b573caf2c0d8d2b504c0f7022007e6fd53a1428232e90bc8e397a1f136e4f3cfa9bf83cf1b09eedf08d21db47b0b8af06bf2e0ea8f3d175fd5a9b01a78d1767c1853678d2e61087e6c99c0c81e6992bc849c08e450940bcfa9c93c2c56ef22423f57bf420a64367a88946d2e7d85275b6068d157d3d39c835b5efa4abab01ea41f16a01b8c8bd383f27e16b65d39570a4f5e2228ce2f0de7759dea33fa137b89229676ab946aa27a6ee0ea0da0f9048e20e701a316e024c804e65fd9bef6b5b8b18ff0f8c60335e8ddb047bd702ebdfda5d3b9e2f3cee24d04c32c5f6828e1aaec238eda711884d5d8c4a9db90a0a60b87ebd8e3d76a546d6c368266c60f1d055384d295f90472625b15b992f11c619a57deda27d918a672c68360d6f295bc817bf508dfe0d3ac88718e5394a004a0933ae55d171d47973a5149dd13d84b6e2aec983c382f23f07c073f6ef5a20b272b58f746c9605d0c83f5277a75fd75af7bddad8c9b795346ca82b1c8aa17546514de81a9771902a38a7cac70ae6b6e949e4553826d8a01d9a6846973e697d72d8676ab8f8edb17b5f601119ba62ff06cdb4849c8467765f56cf8be7bd8a26a2bf1b36ab63b7a27db78355d5f1ac476186f7b11d483a931ce36bf95c0d59040bf0924968a7626168ba2a3eeaa3ef6e66283b6904507359fff94b503d55f23c3fe442f73687921a1c1a99ae56c28255e56a62d0447426bfceac40a6d4f0f409e3e7847fbefcce7abfbdd0da0d1b022fdfb5844fd312af9e6369ea2c8c449d8a91d901c4c7658402971947d951da7acff7060f644938c555b4fb7c0689f783138d2c30f596fd6c364f2ebf5b09fd9987b4e3d60174a8a9d5f60cf71150f1e3ff7b9cf3586eb75ac9bb6dfdcd9bae162b6fa135e0b325e98851d3e8aab06933ddc0e5e865bbdc5da03048e55b66802a833b0362a296665a8e1495b99666fac791bb7739a943042d31a7a2d1f5a9a529dc180cd5436479b0b9265e9540d25016d52e8c4afb8bba4a33cc074fa86c45e80f3280138a120aecbf6386f03f2dc7159c78501dc0700fd22b409c467af64625f8e3efc757afbe1d562faf719f79d10fb177ea07f27c1ef3f5c4c571f4df791817ce4bff8a6837019a15ae7c8541c2e20e1b194a737cadd20f01a668db6cb28330f09c7e45d330f53fc6bc570d4d617ab08d28ff557315ae6ebf0dc16f6205d98691ca45b36f83e6cdf790fcafb59dc6d7a7225c00dd957c5c9a2dd289d0add1410bea2636820e70435548ba31ac63e3a6e94d64a92014ef08baa6e8c95ce9bd6dbab0e2ec43e3c5e522f2a871efb0ddfa1958700c1ff66b8f9c63e34ee46c12a3b0e8fd8b656624ca7bff0cfeb4d623ce96548aeaa9b612429975d25300f941e72af91cac82934f52f80f2a9d610d6afa46087c98305003c7d88fcf8c685acaa072c77c9f4951ced3909c2b169ca4e83bc7bce22856d0c2e45a54f7c10272b01f05fedacafb267ea6fc1ddfc3c17bdffce60dd3e0cd63ffff9cf2758772bcce456a53399f4d574ce8fdb43a6e8dbf364d63f53fcb8bd6701a6f85696e5f394a5d702017c32f61a7fb577ee80535fdfa45086961f809ccd2869e7ac34b6cf7771d0da8bd6dbc85768ca03e09dd9bbea6151691e9d1679db8bbf17503f1037c63c6cdb8bf53d901ae36ef35b095c2d098c02c10735217d4692715defa4677abd382077fb3470b32a769a36d4681f2da5ce2696396833be81cff6b931489f5600f1045a1631b03730b44cd8ea3e6f764fa25dbcd474313d01e21b31e2dbb235908634b7b187b4335b8bb14220986b9428c1044fea96aba704a100f83d84c44b540d67f372119ed317f92cd2646be7af55182e1689587ded13bcee6daeaa07eb9287d1dc0c7871cebde4db5e180083671006868cc9c1e4d806580f9095602bb846a0e0062f13c7c029f8d61427b00274083930870b1d73d95b282202f32292dae053a06439849478926745ee0492938eedd31054f2faea04b3c10c85c06beaac23e738f80cabf21090837aa040baf6a002d6b97791338ceae8a543ab4fa07377b267f5dd8c293ae476962b49f3e9f416d990797b575133284afe5b884c4492e7c64fe1144a9362020ce191479af636ae13d6dc207c1470f5c062ba7cbf1770930ce7fa9352a953aececb1d75e04bb59abee001d231563d387a58902e99edaa7913e653fcc49b596ef0d65eb58b63341f9ed535358bb36a14c5a28682e60b886c1a353b6938c0f5007164d2019f15f753dc1cb2b0b0c475e9aca25dbeeed31f41da9b5f647d24146db03ff9e697f116aafdb5972dbdc92d0ae567b4011daea9fc453c0b763a87c7c0e1c2bb6fffb2089155979247b6b83932762c387e69669c0d46b14c77a14ca8d101e114223ce8e54206bc7c4ec9ad2445cfa2852d8ef17165e5b970447ed6569b3e47ca1e48e0d6eaeb82e39c8460d50b353895bf2c3ca49dbe40b476b625388c69979b85bfcb3b9d5e82dc7f00b99fe3a6dc601d0c56d69f71f3e433fc9196e3605d80650377f35723113bdf5a17d7681efce9c1e4934fae1f257dc41fbdb54fdfe4e0e86b06ebe84dd74c5fec9c3076f52c5b5cd0c1488a43e973bf21a33e1d492f97f5883341079da77f0fb49e735a4217ce1ee0971d3fa4843793db5e689be535549dd7dbf27b85d9f67202d483f8b61fafa61fb64775b38f6d792b81ab21011679bf3c59e99bb5128c2006428eb37e27807e8e09e857848babfff0c4210d0a9cf01d98b6a7b6db2cf6564dd6ed01664b9eed0da435dcd67fe233fb81867fa47fdf02e267e00bde08e20b3bbb85232e64c4a45dc57795af157c68c4961d1b88ba07cac3389b59d60b1ca92f466c127a1c8771c6a3b8fc3e9d9c5f2c171fb36e482ea567df0be6bfa36bac5f1c9ef9ebeb2136603ea24cc6d55be0ccbeb9c90c1e64b6d84582e4328d1186fd79399757f73439ea6033ea1a22c7d1002dc4117ab74663abfa05495af8bda4d0c8604a516af23a4dda8150f485f1919010fefd9abab82ada9908262c6c843ab9ae60ad997c89111cda8784371b1960cd9c7561c93664f8bae4bd10fa3e487d27907710d02648b7faa8e41693f1870bee73ec54fec352b8b035e30e009ea88b2285a8021a382d6cf0c4956747909b10b1d6cc624412090171df6eaedf99f18b5bb99008f3ed2e6ddbcb1f238039232c69b4ecb9f9356d47e92be5e320bdd76dcf4f4d09e0c47ebf8ed314a71a5d52c34cda84baa60d82649d7aaf2205418523af630da08cb797624d10076597d70c3ebb146eb4a27ed8af1ea55f0e5fd133fbcf57baaafed18f7ef435f3d90c072eaf7ce4d3600d1613e8a59891644cf145e0c57e6a70e0818f511992bb0f39db5fdaf8f38a45fd0474a018bad54f939ca764ed30d7822a024b3869ff6d6845425af66ea2afc89a6c39a2e2c371a43567ffc38bedea93a1da44ac569726f8315991573d59c618122152f3aaffb00e4446fcaabdbd0b3fcf2b1c9f77f2e4821fa73a3e6f01fbfce69b2733eedebbeb48804e33cf5775759d7ba4af8a3c3bbf7025df15b0cb77f9e43eb7ca94458f48270fd93799c89b38d63bf7d95b4e905e8b38d2718e6b6ea3d9060c44e040f8b3ad8d6c8b16882eb49ac55eec13bbb95bd4be279d4cfce9c657de80eb01b25d4e5c2b7253a3afd59fba18f26ce2f8c969e9acd3612b6aebda6d6e2b81ab27016ce30be54362693a02eca4e5517a3d21eb182de951b4c9ee3738ebb3344bce46edc30289a824fd4f9ab7f2b0b860051d5b57df5e8a6cfbea210f988a820de6fa339dde430c759a2f327f1cf00b6d97df93e02c9190c7a60dd01b0896c86bc0f09711d91fb69d5e8326ff85ad3f08a136b661c1407ea4e159dc1c8a06743e02a4ef244e7c5e4ee0e2de6a873df4974e7b9b0f9276bb3fecdbb4f8a54e8fbb9b3faef01c6c04da9cbdfc655c1e1cbca98dcbaf1e8464ed4358061444b16898a9068eb47480cc44c64b95ab1c156c829f7a1b506bff9e689215a1561d89a775c31bf2f6474bdb9951fab6314fb2d6f1e422173822ae26d60423c5200a1a94835a94079e51c8b3f4ff7d70fd3fa100ef01eb92a902f305017a3ddddb9171cec7608587754b493886037937132572f02abf4918b2914b3f26d9ccc726d65b32632e884d0e81333ff6b59cff2bef4807ad92d028c1cf1f464646540f0b3e46f47e61b6def622ccb7bc6caea60bdf06e94ae1699466931bcb21c2b3368e238adaa87456e09963d5e6a35d9cd54ffed1eb40bcd136f805041cbbb63a85d84ac12773f6a9ef7823b84370b22f8671e70b81602278b658a4ee2b7e5b62fbd395ea1641fa75dc44dfae95c8b5be35c11825399641cbe1b88d418603cb993cfe48032a98541c8b2d08d52021e5d00a39729189f8e90c7c92a2b0983662536f4be82698941e0852978c17b6f82d00b5a024be7fe93db4d27300b632d54a55617a14044dab99425beb4eab8984f9a710b8fc7432b6132b0dcd4e5fbc584cdffef0c3c7bec939b3b682f513f3cf10a99384e9e51faf60fdd6e17a2993f2ec20fc788aec0cb0532abd554723f09286c8f3e8a3572411db3c5172360aa2406c46595d40f04ebdf2cff0e8a070596914076870c1c9cd9a9477a6ef16fb405a7fef9dafc05d04a9dbd4dd43f7a77e1a02c71fdcbe3bfd801cb7802744027ceff390c6102d47d1634a947509fa0c20cd5fe85f2a2f3c1f2d439bb151fc1ef51a927061d2950e39935e2f8132c47d2947ea4033164a2de7295b4fc493849dd4dfe40c38d74f278b9f00e316fdb07fd9b511465907b71f9b71881dd3bfdfaa555829311d226dbcb2d98f034817f66d06b80c9172b391ea561768c32fc6428b46ffaf55aed4eeecac860749eb963df7e469990337eafd41527cc29141fc51dfa6c535756a0cf1bf4c80de00594988d0948022237128d914e3858a681bdcfa41105017d7758a0aa2a5532d74a3b9e3b12102cac5ae11c90393f4675187190589100104274d32598ad2490b2d270105088c83a8d686a7deb10ad2262475623087baf1d0719c36910849128ce10c5f1c7c377d5c32405faf9af78930fef0b39e176e4e9e5361b7b4ab5703a328180cdb6de447ad0a63a23aace666c3422e4ed4d500c3b7d46a5c1945312eb5a2f991c564e13561e26fe3e5726b81e483a33b3bd3e77157fb1751deb70b73001240edbfa567b4982ee8d0d457d3fbf950a42df0292781e5def2e6681a4aeecdb0fa94d5433945bdb25a8bee45dfc4c91f707549952a2d8d4eea217d2a3f38aaabba8a0d99b0e984773ca098b2871e00e66eb0ee0987ba8778f3cb31f648f3c0dde097d4ad2b5c51ff5f67f3a89680100000400049444154e94dde3cf4402e3e65e44706a3a6c762519bb1c0bfed12b079c591255930df7c1359577e6ac5a63c631a1bfc835f61214831b96683b14feb0b2e55492bc374c1d1bf0a0aed4a7eca0768b7f17d102c2cfbb771eb4b5cfb6ac45c014aa5ed3377ac2e0191b97cdb487dc7cda4d3562e329fb0675b49d0ff0df4f3e6e3c78ffd609f2b83f5139f3f313f71e284df918b9d16aca82706bd5a2bebe8e057ca53e46027915fc9241d46d0251fb3caa8ce300ed3caaeef3d171e1993c9d64ceb94a8f36cdefa1c9c0ce7b86641dd575ea9a28b6a232e33cc729ebdd3ee77d095f753bd4eb8792ec4f97692fa38fce572ceef528072fcf8a1417a1a9f3ebd6fdb4b5f555b13dee6b61278fc24b0dcd9f9a29611df8129b8f5232e45e7125dd73eecbfce42c5af3ff20163139c735dc0878aa33331ce31ec0e7589925a4f699837ea096c75ba95c4710d29b6866913f210b54c09d26737c4a9498a36bad5f244f62d6d60d53b256120345b17398ba1c0fb2bad1357423f7e71cd99f61d829da67dea8a6acc75f54b89f7a7a7a376187ee428cb4e317c6b8cb878d6efe80749adef49fbbfe48afa7df7ddf70278b9451939d6c822a2863d0b26e15de05ee115162328dc3659e23850d191b0b99a2e6566b9da0835205590060a79f829518301aa4e1afad4192cd85e41559e0cc181774d25d04c47e80668d4109e5a3f32102e001b90404ffaf29e1ad033d9f64792bffe8d420b9ed9ea32f9efb901b8dffaa39241faba6e1d9cbba2c2bb3ef9785ef075e7f4abe4dbcb024cf0574738cbf802a53efb651bdf7e256e527aee34ca1da3630459f1652cca297825e3106ef4c1f859db9bd49af1b6171e1cbd95fdc37f8f1e7ebcbf3b5dbc6b4691fae6b697be7f58bc2b0ca244dda6a7880408546e5677f480396b6728953618ed51dd0c7cd052554abd8a6ea96c945dbdc84d61c7697a185d942e99e427ab9bd621ba814cddb62a866bf230690e1679d06eb75047db5fba6e5dee46f0de7beffdc3ac74be261b17bc50947f8ef697cf0953198f3854933c3b1ec7695216e4b0a37520271c2c60d6ebec830b5e7fc6c67264069e587ce5407bf1e8453fa55df2175f65be3a1733242b437d840a3b38a7f4821fb39df9782689a703fa362b0f663ccbb759fe048627faf26c003f504a35079c48e6bdd5d4ab7281cb5ff5b14bee7bb9c1fa3fe0fb2603768375fb1805eb992f83f5f6c912520fdec57dc469357d769f8db4655c8e217e5849b472ad6a53578a88fb220fdf8e38df3e064ff9f36f0535913bf8350f548053f804dfcc99face91bcc1b8b4e959fcf44fd962fab3ede42ec09590026acd6a5aaeced1f1548c56d62d1fbaede5d9bc3bfdc10733bd475da0ab93ed712b81c74702c42daca893d06f638ec464140c62b51c57adf509faa20080a5cc218aab8198624f9eb519706383daa32d2d078bd84b5ba42cacfef145b13c9b62831c241cbb05859890edb8cfc5167725a11dfa89edda93fc49dc4a9b4a938f87ecf6b06c1d4443d2bec21b15da78fa16077c116d6c7b0886a42836141699e087c863cf67178bd5fbc4340d417a0a010d874bfd22a948e337be583eec665d0925e1c0fe784920ecc18c4ccba56256180ebce3724e5d3930c1c1173df80174d9592821d88cbc7752de69090790c0380ed47ada9b47feb4b1d7922727ca5e046b32ac50a671a8664872a94756398a1eede1336f3c61f20bd626c00669c665cc205d4f2e6ef82a7af0f0b328f2f7d3ae9eacb2cd46726bcbfe20bd107a70ee7ec4f10739ddda039fbce28ef13aa68cc5fedb58f23539a422f726a7dc6884691ae42c9fce0b63ca4d88fc5b9607ea3853e5d746bf20e4605afee774f85393e5e283d47db0bf3b9d77f01fb9ed6593c62358f1dc6cba2d3f691258121069fa687bd4a414509fa52d68835148ed883f75caff424ea974cb9ab40fa9289bae36da195dae1f3d1a86e9ad6cc533ac30e4b52943d563ca2c9777f0a6bddd0463743ea7efbe72aa9f49e0ed2e060781b1f90062b6dd29021f26f54c926f0d26f6c3b946a204944f6a83133a814528251a9b1ad8219098b07ecc3fe8442692806291e11ca32fda52495d845981793da04f4f6139879a97b0086f360a0da9db575b41777cf645bd339551c0432ec0c06a14fa3bf394e0d30b74ae8d814818a9e14fa0f38df886b7f3d8ef8b7ab09ed5f55a5907ef6a6f8599de14dd6b176e3b90cbf8b736478d71390fb7ca37f3943340c62fccb94840ef35c340de074985fbf10f3cef3c6acea4e6788133e8dcf8e43a42a7fcd703a6f6c25f684c7e8906f9d138dee73ce8b4300b75437a313f1ae78f1d910eaca83feb59824f7b18d236581f44b1cd3c411278e94b5ffa25a2dbf3dd1f24288dff47f5752404ca651dea3e4c751f8283e965bd4cfc149536a5d4fc8fed0158c9d9d8c636f9d13868fb27fd326dfd908b0a09d5e9a8ce9db60e2a0ba8daa74cf0714bf2405f79e97fb5cf7597052bf4f0e18dbc14689840329e8f6cca3494a704ed82c4318557dbe825f988379ddc4d21abe59c937269e31a572bb659b7a8faf31d6338ef6b3740c91cb69a6efde0fe18e01f55a01902fcc9b205079dc42a4cf271a20d8f3a73ce83683dc84ca9c1ec20386d80d28aa0d2174810adeb8d145a1fd4e15e2326fbb454cd6b6dc8c9089dde316575aa8ad2946c46507d795583903dd491626fe31e45e937850c1120fcfd224efb4ddc391d29d4f5fef3f5627a5f39efc1f981d7f3ecacbedaf16735081e97b9418031f853b45134f9841f616a84634bbe84d9140c1c15d5003df85ce0bde168b2c9d73ad41078bd85ea0389b9fad3b47e23439f13e4fc9d8ee0b6979e3feccceabada3abc0bfd4af7101f466b0b7b722480c67ca57aaedea030ea88478da320cd98bac5c43e821d452c9d440ba4500f5b5643edbf9a623d1a75b63214cd1ce3c95aa4dec0a746d5951d2da9b7babeb27e0015c07ff80f1f7fcd743e7d49f14ebfb1730c0546e445363246f96fb6a15989af95fbc6837800f1400e2c591008e26709e4196708495f236c1299978fd270cb9ea9f39f7eca4741c37ec243f15226adbc1b0e80c83ef69e5c984b93d4c923200e3563cd8359075c0e103bd5946d248c1506b9f42352aa29d8a7f901916c02fe46992b5fe8e86f0d4e73d3c1b7acf8a89f07f5cf1bacdbfcf8f1737d1b8cc5a1abbe9acef9513d644a97cf7208cea1fa137f28df004b467627069d2af30c583ef9082620eff3090480f3670beb193920a17e18201901d4d1ce7dedbd3f07645e4caf434ebdba54a5e917a6d39d0f4610c3033fa5d32cd8f0b6973d2e06c71025db5e8e08d2a5fce0831ef7bfedc5f261ab69c2b7692b81c74b02d8fac3f121bd034d25fb19f52918429c0607dd83450d48fb143f0d5ddc11adfc8b5b4b7a1c5367cadef8facd5570ec800f6db3222e7de318db69b3dabf28fe5997248c4f07d8b97df6fad000e63975d0eeb080d7be6f1ca7da67f52492f9566ab4abbea0f1150d035b7f67b21cfafe74a3403cc1deced2379de506bda3b82d8ec8fdac5be3122c1ef520e951f61f772435ee22fea0e71204026a8122a261c08c9872646030a86f7620119ab8362c090d2b411203c95832c81cf3d582c54c081703da3919e248c27ba8760904969e53ef8be9edb2b040b68174f5a0fce7ed261203c73ebc0c66459eb3685955b783d0f76c0e2c8702ade2836c7038ac26f7d1fd10bca6c1e8e00afa3a48d749d7de448374b68f6405bda3bb4a2d9c726dcb5d4cbede91d53e2915114ebc5838a40c0b39cb9089732ea443b9e4e5a5570c8dc0d59f9a1f145c2bb28e813827d063357d7ad86afaf7d0ea7560ed22869f5a2e670f341eedb5735af9edf1ff8f12f84a75c854ba634e8529bdcb8a226583144d240905cd8d61748fbcaa46a59a68b05838a5cf095a018032fce85121682bae34d62f945ee4cba6821f3cfa8ac60e3d6aebcbc73ef6b167e1fddfe048b48504c8726d4b7c430f988b477072530e8fa937d04b48dac62dbe38f2ed78eae3e8944b64d287995ae1855fe0b2b93c2303a0c3424bd901d1be0b0e94427c90708d1559861ff2f903147f6003607d1b6128386079f244c3ce5b0bac29bb32557e553c1d9b64fa0d59b52bfae59f2557732a9f528d5fe42497fcf3b5f3ea7f5f4d176fa092770c5e3f04eba3ad304a7562b0de3e8f682b0c6d4ed2f54939d5ff85631925c15a6490873e9920e7356ca6923c3df3aa444b35f70ea14da6e3097ae41a16839f6d2c996967b8fab399c3cd4d02b0d22360b44d4b68aee6ab7f97c51b23751ef841ef72e1b5e95109fcc8c2c59bf327ceefdde0db5e9efdecc583a36d2fb63d6a35ed28ba5bf85602574502abc917cae639aaff31841c0a10fb42f739679ddb98297e459f114b0d5e825a0804d262b4848b40a4dfe394c43c52a26dec2a5da593f41fda81d126c62f4ff41f98073eb167fd77596f9c4470a8730c7c740150c89f63f2230f9cc28f7c863eb0ce5bb0691b229e060ab844c70d643959b8ede53da9f68007e84f92c619ac63f42cf2c27b5bfc3c3934392a73940f889c6cc405e6f90e41217566154c84e3c06b7494adafcb7764c8200de1ad4e906ff4db066ab02f2c78911248d445285e4ad2974fecda51358b28fc0a4354e015b053328aaee51f6822e22054c05b9364e04dbba0850128c8993091f9d847cbaa22f66239173832f209ea79f8fb9b3438c7e7403248bfc8cc6442dafb322b40af2d2efc5e7adaf4e01ce7ecab0f336138ec3f053f5c90e819b9c99f6cd505d672460f40b88a3b408a6f008ea8e0194efab2a569997dad8e113a90e07fdf6a3abcefa0bd7f1b8c57a5b7d5e40106fc8faaf5fa786136cb85650d3177f0951cdbd5f4fd127a3a94d80f771d0a72525d8feea3f07158961980ce385aa60ef1176fa073b3527d146ec3525d005650957665978da2ad9f9dca7d875a7d14b4dea1be0fe140e1a815f563bbf33761c7d727180ea7341dd97a18767cf88de2b2f19acb8363926946c829ed1c97c9369d0e749581c70afcab3eb2b39d19cf75281c7ba3bd7c652fbbc2213593065acdeca37344ad1855362054ae8d9dc898bc3ce4bff947a8d03dbd640ed663ccea1575e99626f9d60372b2ea48a44d165a2909ac4adf076f41e7214dcec1240f64c2ebe45fbd77e1fc4ff3e6a71b178b6b77ceb1b20ef8c0be756106ebed7c45abebd28c5ed1c80bac2973a0e2391fca37158eb310bcf1caea9b65fe23e55489ef7ca529949411b5cea9376b9e81d5e62be44131d3d827287d49d219e4cffead534ecbe9bbeae1e844ea003653e977f7f9d4eefb86f2199f7fc66452fb5ef635f4027dd46ada3ec46d612b81ab2e81d59713d7255ed336ca0fe9028615e53800dd7ed943b7b5f23dc264aa6c35711a8078246d4b3fe2d9b6ba160cd3dac43d36d4ae5a598bf3df9eca4f353a407405a1a9ff32dfcbe2b772716efb161b9ac58ebb774c311dd84f38e26c5e9edab9d3f39cbcf042136fb69adf4d4d623ae90d77eac3a56d1da9fb963c718e4a57b23fddb691b73f74c405e57a01f29ca0d84aa46b312b0b9c2d281c8522bc827af2c2528998149808a6ee01c98a5f813cc249278597603c577d3184f1c1a9a61f09db677ca48138fc00cae351a973c20cf803846d91b9a8709277efb732026009e27b07284b5eb9289e303bc920bc202dff2ff8bb3fe08d83abe886e83e38e49c2c66f5a0a82b25b5c5e542dea242fee17a30f302fd2c177e983083df6fef1709fb2ce9222dbb6f32f1e2a0fa64454ba55620a48c1dee1c555a3a37a059cd29e73e6fa13d5d9ee17dbfbf685b13f46f636c6f66bc2f0bc9f4b7e48703ea86a45f58b8c118148bfda879df6f081cf292ebedfef492ccd3e9c8ab104f67c55bc5d746d4219d7074095d346fb2ce0017655155b429952c4120b66280a4d7151c22b48b6e829c155ef4187fac4ff1575e92b083c19515e49ac94594ec14fb5ff86670a3ae35e274d88afafdf7dffb0df0f22ab1b40883b266ede1d708b22c4c3ba1de004d6b8fc1d382284dbfe6d8848b9bc0ce9dcb0eca366d2c59f9c94043346d223efd5148da074d20105f65012286b952566e09f2c9cb6b2c58c1f7842cd31ebc083c42970501e2573ed3449dedf3e097f03677f11f92842ebb146d5693c6e24543091f0025967abbc905da0ca9af5e55d1313187e200c874d36cc11649e6f11df8b3175dfb85c9ceb973e7e67edacaba648c60e5e01105ebcccfb394bf4d158df3a170dd495eb3a30ce59d924c592d5f8e858659ec711fba5580ac375374c0cbb4524abd334ec6da6a4eb6fcae5b9cfa36a70c1e1ca4503ccd6667e96f78800c024382ffe1478e767686ed92832f1d109fb17edc69fc2347db207d90d036f3044b009b782876a0cfa76ffd567c00c02c34c4817010883fa94afd0af5695376a567024422e7597fc839db2013bb199b01b013cede04e8257d6d98a02c025926b52b4b752540040ee1105bb56ceb18347c5963ddc083fd5bed593f61a1f55b7eaeda04e4c18fad398706259b0acfd97193aaffd5bf4da11fbc7219ad5f6c6f78a95f231d02f98ed6ce47c12ff98d5a1c2a045e984c0645c90bf088615974001930151934c58c4078492363b54ee799f6d528f0346f183afe4c609344edfbac3b28c91a2d78916acd61873ec155c04e60d10650730a9c7c3e85a3c3e7ebdff375b7277fd5df300ef90307ac4e2263017696c8faa7acd94c38e2e1c6898b147b8d6611b85b5d2a483f0e8c6d2ebceed0b6cbe50e01fa8e716f1281f08b60fdc51913fd2ac55a418217cb0a848fb231957cc83b34ebbd48f555212a133881e7306cd12fd8436139790b17d43df8dec150be13c49f00e7b9b133e53999fcdf7c5dfc769ae695629ebda1b8e2654e1b6cd3d34e023b3b93676936cd35465d6205ea5974adf4529d8b1212a434a47e8acd188266fb16d5ea62342a67fc6e545888410fafd422e1130ce086e40fc08cd3975be1a1871e32c6de973657d42d4f273b7f5feda74baf0795d389c92e03c9870afd888196781e12f4c5412803f0b2c21aeecbbc1889415ff0a033b7bdc6680f1190798345ca7c1c6a335950949927703c733390bac0b57981922227af26cbe04b89dad6073969c4c84b8ed5d4f6c28399a6d51e74f08349752e7e8db654435b466c4bc9a35b6982227ef8b5cf928968e91e3f6c4fb6918e3771fa6260b700fcf98bbb3b7f9673e6b507eb876d8591022bec975c59e781d51bc31fcf0494f46964e7ca2a1fa9d43ce6022f9399df36bb8e039956a02d8cbfc06adec88ee6ccf903d97a037468f1b841da0a77ecb507be68c8576e5a7880cc805c4e4ce8c3704d28483fae57d4f0a917267ce3ed620eb2c11fdfb080c6be6d2f477ddddda96dcf5b093cae12984db87b8c85d30dfe4effa2cec7f88d35c83487a51de93a9a0bc25034a3b25173da6cfc090879731fc889f7bc6a80ab8769cea5ecdad61a1ca9c743d2a0200b21695dfc0078ba3157e839567b71d35cfe05d3499c2eed73b69aba06b34efe727da08138762f4e658a4e6258c951972ec481216ad97131fd15bb3241f7e231bd80c13a1f6ed22faead9fa748f3fef42cd23eeafde9f613d1d1d9d7c94d26c603ce2b2e5ade41aa09e35c12094c51a556698ae788d2aa84927dd7caccfa54e980455550d534b24b7b0124f2e2f8514275612c785ee133308303b5cfa075e70d5d98a2d579b6b4df012fdf425f77d85778b39f786edb792350edd3571fe072f2cf8827d64b1ef24162e59c85f45a4b7736bab3ae205d8c4c4d56d18fb3f5a507e8c8f51cb8ec24612bc96cf2e7aa2fd05516b94f746dd9a12b5107d0ea6a70918572f2e255c13a6835a842a7e8247a91aa0b98f29d9d99cee7bfc8ddf073b8bafe63f616bf9a3e7ce55a9af2bac7337ca3f0bfd1ec40baa676d2ef835f7bedf9832b430d63bbaabe4f544ff902b670abc1960f1c6ab2befa337b7ecb76544574d123a9e969f4269ea2c07157ea92d8da691a89659ba865d96ea10f1bf3fa1bebb0879dfd0fc2d723a5fd158d3c6833bc4bfdb0ed55d79d3af50374fe42cd599bd636e0da42727a84ec479737e15659eb3979ebdb2763840206146c9178b850d9983c971f6260380dfd8e2819abb6aacf021019e662465978ec513f234f16fcafbfd011d03a09bd10942ef08e975eec30a01caa5fe9d8dc23a991aa8b4e662be09a112bf51d8c8fc609b43dc7df304edd0e15d54aea2e986466c34568589f8e4316b8e5d52e376a77ececccdfc07cf9ad4956d7dd2277e2c4596fb6e4da4f56d609d4ddbb9e2d75e685ef4f3b37281707e329724730f923788e7c1523958a3972877cad8ed1007835b7927f4e6836436bf34a25a0d0c956a096974ee6a7263acaeb7c98ecdbc9763fbbb4f9bcb36abc20b71ce7f1ef04086565a607f38b93d1fec1047af37d6757d3b7c1fa3e916c0b4fa004b0ed2f6bde5930256320ae2fd1351891c73e7cb8d4a46f8bcf246bb4deed26fe61ed45c44b0bfc8c143425fd4a6c2a35e563d2226d41b15f7d4bcee92c6573d5b66022da04b4b42968b77f7a85807e54dae2399eb4e90d067fd63ad6b9413378b4cb4213634b1fd202204f10e66ffaabc675967a4a9cce21013bc00bb5fd191fb03b990eef4fe7f7322f932ee503f45d0ef80535208e8e5398c2d0512e09ea087e73175235d482e17f106be202a04d5f7d8a143d04097ad0ea0f7d4a37bd829ff14b310ed5b344436d5f3e4a0132324668288199d02f05aa89e1e1c9d9e4d5f07ea7d70d14ef4e2e66b7c32bfc8317c5b295ab81f625adc601af2802e36de97c7470bb8b1350937171326b4eb807e9f54ac30b84b76e73d9595c9c5ebce0127a82f346077e4ec0c32becd4eba37d867b35c34cc6012cf2e08c56c896aaae3ce4d05532cf4d7d7a134802f1133198e7e2c5def4d56af1dff270e93f217f5b19578d1bd91123cd5fbb56b6fddbd147af4f97b1967c13c7fe3dea7d95f3b040aab7da9e9f7a12e02696073cd53578436fb4a3e43da343a59956699be2a17fea228ae4d966d159ca067f5d3f05da460435300fde84f064fd62178da827b4f0c2f4c2b07a7e6a84d6510e3bdf77df7db7e23ffe463c877da9fb5e07f8e8561290161bc4db66da88e4d51151f6e318bce0b8029b0b54fc4905753e941d1c711d8338690b47d201e42883dde42241c9eae7acf3d8e982d2da4b853e05841bf220966c7b9fe052cfa8aadf20148e4443992ae984e560da5b8886dfdcb0b7b9c850713aca29bd3a16f3a1a14fb1a7aa0b97bdce0b15f92a2ab99e80e96b00d83f7dbd9a2feefe392bc6cf044ab07e92ad3027e7fcd6420fd60d69739de9017a0fd83b45cfdc38dca85c9490821cc6998e3d385e277ac506174907b3608ec98f7f36171b5a8ace2d2f09f2855a1956c49796fd088236c81d5f1cf5c94f948b7e09eed1d8d93d0ea607e606eb96fde6127d8976533704e954259de48bdacd34def662dd76ebcba684b6e5274a02d88abf961ea3d1e6d1fcd8903187febfe2106d87a40db6a48bc9ea768229e102ac6c34e228a105cd784bc0a9c6eea4a9914ab5b9aa5426ca29a4d4173b608534f861d43e52cd918abab3809c7c8b2b5d8868ef9efc04bfbc9d6d73b301be7ddb4e9fe6f2747cb3fc854c3659a7adb19bbce0ef7ec9f63de555acaea6b7748145dbe3c72ba6e269c6b6c079f88dfa787ffae59e5189d4e9ffab1d9103e29f04533ab1d5ea977168df41915b03c019540dd201d52a9283423cd63b60fe2a6fce64233dbb9305c44e9cd8de19a3cfa4b589ab26b4b19974db6406aef45aca05141a9998d058fd0e55ff237bb9efef38edfc0e9ce8f7d3c9d9f42910fca8950e5ad6c2cfec57c11b56d30dd0b392eef719cc867341f41d27bc704f3a7361905e7bd175d6b5cd655841b71f92fbbc51febf429f27c33e435076b9d968f218e275e0f2166584a71a6dc93497548d2432567c121157ac522c05cf0dc97924fa6790dd5fa68f6c3030f04840667fabc93fe23af70979ab74dcaf65fbc5e511bff165bba2dee5f8f438a39fbc2925aa83ae943e753dcb08a283e4c4e14f332db5a9f1c56953d96ddc4c397361d12f55540c95d446d7554b8e238796ad2ffb97d507b44b658e1d9fff03fc0e6f0721c93fd15402d3342abb2ecb91fbca198099b495f0451e3954902e8b8a814fbd09c4320e7b68e325a652b56d41a442718c2266d8067a36a2e01982d214272889fa524a7f1112159ab16494a16df591dd9f469c25cc90b45ef987bc8d4c19936dcdc2a1b420dac8a68598356f9e49320462bc72c82800fee930375fa0c427e95fc4a75041bbab63f587c30badf03399bd8cf7adbf03cc17f131769df0d69e1eac5b541291c62858cfaa7a2f23ac9bf55106e87e1c476269cbf0e64d8ff2f79cb139848ca311b6812995ce2ddd29f356b66de62774a9073ff3c261ca2b3aa98676b0aa4e38ed73b357ef61bf871f5e39574be9bced65e1f28d43cd70d3f5e6e19a6b7cdbcbc9c9f9f37ddbcbecc0b617db5c6a256d93e6b6bc95c0d59600761f1fada9684dcd92b0796d0313d207993c09c036f451540012a8df287f500d5a9db81aa0c59cc19756409ca1110add09821f48da81167b046f48d536edad834e6e2c002784a3427f9454ddb69652956dfcba8e4d86ec3be42c9370322e24eb837cc9a07c6657882d6d0c3e9d79c5ba5bf49ed868911f3be2b4b762db4b6d4f3f4e3c759ebf03691d676d545dee463dce9336cf7182e447675d7740d3bb8e1ddffd6f582d787e2e1e0a93413a640750e32da104a633772c0eb4397805695d56969064da00eb022a1171d4a12a887050a7f0105c286422e93efc55abe00aa7c8f13c7dfc208ef6b3a01c96de0f17df4dc7bce5242a33d0b4a442d0c73b7a70ee791d559025f5207dce7b308fff7fecbd0bb075c955df77ceb9af6fbe7968d048c28ca931928d14ac40304405b6120cc14aa8a888522641c414c4b6848d02251bc7101b883c8860214311993819502102c818612b6208a1201af3c894ac727808f1508d4a8334d2108f32969034cfefbb8f734e7ebfffeade679f73ef37efd1cc37397defd9bb7bf5ead5ddabd75abd76efde7b331a38eb277cd5338c1f3be9bd721d747f843f0fd7bec576a6e39e1b8f61798213510279e9a30c92c716129f432e58bcaa90a98d1fb22c451b2c3d9bce0eb8c0fabc4215b7aa9506d5fd2613d83f1ddd9aad7a4bba5afcacd3fa6afa1863bba23ee6c665105f2c3eabcb0c1a19f1aaafbb45f21093686c939b8a8baf88294b4d1c2382ca5f2e184b0a936f46e823d48a3156efaab3b972c0d5ffc646f5b31107e81ffdd11f7d0d35bedc1a167a59fec77450a38e59b361e57065291510ba43436d6b3980c4f327593f820394e23b7ad5d1bb720835da964b5fc4b73e82f509cb7bb7adbfe9aa5b67847b4e53202a5deb2266d1043ff6663d35d1995bf515b30a77c0a6fe6c427468a491f6347c9282ed97f4cb86c0194d4480d6d3caa57e689be9f8c4c94e26496beb708b48a0da4e847cf073dbbbe103b38407e959bc4d9637f090e72f9033ec5befcefa6875dd419ae89cf3cbb2533f43e71a9d6b6b719ea84b16eaa22f592dcf995cf2fa3e7413e57cb3c71c023e1320f341095f7276308c086fb220a0c6d68b30e381186bf5d94c0bd10e189bd874f2dbebfb5d925d877e01caa547b7ad7c7997d5b4b357d24625e3a43fdc243dc6dfc6b71c78a239800e5ca194ab7b2eb644e74d440d4a0fa2232d4f487e1c443391451fb5163d14a6fe62495420e81921ca19eac11734d8c7d0c196743cdba20d12db8a52d0cc4a076e3cd624084d9f8dabef60809e95722009499b878533ce390fa74b966259ec11464293916dc6e5e4a68c3468d1dab6975df6a787763bd43a7a1db3391d78bd3f5dbb7010dcb3de9ffe482ed4ababd3c9f334ba0e52f6f42d27b75edcbbf835f7de7befe17467f9523b5583269b8b81c5ac625206444aa095b1234e96d758f2214c83799a6149c5f802b780f92908f7a4ecc8a40cf1e40601a71522d6965ce2558e94c467d9b2f2be90bac4813a3fbc9ccc5f45376fb7115557d1c3b1f569fe5fb5a836578efac0a82fc7f4d48d2fe763b7a3d74aface895b912ee5a4f361a013685c435d3f025fd91f4ed7745e88f8931b0a45da61c70871ae3dcb06fe8a5f22a74095236af0942bde9690a7d10c3aa8fbd5f389568593e53df3e5f27b4437f87053c53c66716b95dc88dd7fffc1cec6ce970163bba23eb0e2b288a0ab37acf616969c7954c4eac089b40632978a0aab42d44ec695d31843e184326fca36257554c515ca19e7ebccad2fb313be1673707acde15a8bb6307e97fa1d77dc712da47f28f60959cf9fceb313019635869d64b6b258be6d79a816a61d0049f9ef4f9b5dc688b3e569b77481c57987b6e463d662255320f54673855146e72f38e08649f65dfe00f7cffffa495bde70a680b4cdcaf501e7a00a83dfdd26c87ad680d8be91ca4006c0cff92317ee606affa4e3e28955cb07ff12d2178b0049df52b0e222a4803489d3a0f0b0c13c651fbb34ec8f411ad2a255da6300e4b57a6dc562729e5c9f0f7a3d6f17cabe759d758b5ec2591f0c0f3cb9aa264bf819e1731c6cafa5e59d11f81090fd214adb2a6d56e18be7c592b8e137312faae4b7fdacad4e21220638d269f43330e011220ba94e7a20ed4cdf5339a78ff1d3e9c9ee62f7c44b4f5e3bbaf64ccfd5ab7b4a293cdef6b2dd9f7e9a9f5bc8a79703a8c555da7a836a9de77bd40de259505047b4ed2a9d38ea7cec80e920716ee5c505a6ed28f56cc623f6025c4b5887e544b05c1c629294d58e8920651dead0f724bcb24c15d87c68c40f22ae650af194b308739084d21ef18c4b06facd6e250f58fa933e4a435fb8e125697f3501ebdb5eba0335ba4e670785351c660117bb821d38bded4d8c7178243660e68a2806fa59318479afd7e4aebd83e3afb97a7275d5309f7c71b806e5621f8da617e5245607e48f7b209d98f94fa77b43324190087d5924031c01c6a7e8b542611f30073803984289a70c75062cbaf1e2a4dc64fff9ec6dbdbeb3ceec2b3ff6b7584c3f46fe3741e0dd168fe0799e4c3fc08a79f9e620785353e6b3b2ee5b53c27457d2a53d38e9474718e605befbfa7ef4fdfdc3d12b0d173f4203afef8264f9d4c6b1ef2da7faac7025a7e6a112d06a97fdb3a1fe8777d1156164541fa4e88fa3421bde08a9a29ec3e7e5e40d38e7f6bf857ee5d7d38fedbc5d517f6c7c7baa4ab18a706d642a32442ba25c9c234c058cbc2a4b18b3184e1babefe33d4145cb3c8d193fa50cad0e428c33249455e96afcb8385cbd2ebd5b34b22b1c78c11847ae4326634f7d007229b9bbfb834c16aedaa6461f08b4e6f856d4e9444292a0f36c34524f9a5573ff6c1456bb9c70110adf62296a590a969d0a6a51a08c8e9eb44531a48c7d0c86193a739eac9b083fd905ca8396b12df5002390f0b9328de7622867310916af18d9554e86567fc0b72961b0a820d318d028034e2d31096d6d34bff894d6d8a236293a728eb3ceb941e73b93b119093ae355aa7035d9b5f46289e05a8af1a849bb56d7ad1bf06bf67677df767272927deb3aeb7d759d5cbbe77e9174b3adaeef43860b3adaeaf8f29b0d5f7d8562984b090bc97c8a7aae0b26f36bdc64ad44c323cac8bf3c0f550c6a19f2d03c927ec994b2b52a5f63179ae6c1cbd410c4e93d6c1fbcddfa0dc848e602f7a722becc1107ab373dc45357e4cfc5c0b260737c72e2db5e6adb4b0870184fcedb15f5ce95edf9a9e000927ea55602c92edb81e2bb050475d451895fa71da9fc284774cb120955343a255e68891e6aea8bb09e24d60a0ed3494b3b9d447fd5b9e837a8adde4e575350b46cab35f16b3035d684562ab93ae322f3f39476943e37fb486dad70ec20c50d5842caf1336dbe61c64b4a36b6bd0c5b39f7da3bd5f310a9db5e0e5ce87dc4fbd325ff703660c6d5fd75b198760603c967e8bff9f070ff7e0b9f3b77ee2a8cd2e7a5d1745aa6d8659d72cf1a686f13d817ed7fcd01e4b5ce6bcc9d40826d197a6ab76540d16a891429e68a9c98836b11eb4d7de0d23ea3912122392f26b7927317bf33830eba19f936118695365fd8592cbe8df3cd36c6baf8ff7d1f12d529c79c9eb8cd65d8ea922f8dd64aba174bb5271d277d77b527bd57ec561778778271a6f8ec6f40f865f2c83edb69dbdb1d819afbe50240307abf440d9ee774d0f6218232cd9ff09449b490036b07c9f528e5ad9f6237334ffc5add2ba8fcd52afa86c3be72ab3a62ce7f7cfacbee816f57d4d7d8f4b44f7021f72775c02270caa42deee94491d72ca176b92553e14477dd6e92671d28a7511443072f411ca9b9ad02b90b656dc37239b8de51c4c27e54c7bbeebaebab787fdeabcaae4039c6b3f4a1d54445d5a6989ef4ab0c933aa403ad0e712c3da3a8aa9515570a84071e885354a34cdc7f0fe9840bf466d7fbb889e8dca1e39959e2f8412f93489064a90efef4dba8e7ef9273a7dc09cfa09110fae2d9aaa21d62de21e02fff34463ac9e79cb6690f0119cf8444712fce539671935a9229050e888e59ec4768180787f14ad7327cd0b481560a8e7453af84cc4fc5e4cdf9110f5f445790fc81976b36a2019b9e2cbf0cbefc32eb182f02f690fbd62d031f780eca7ef0e39c3a6068fd39e7c03d3bd778eecab89dcd1c633932d3e68c9d4c211b58a46238a748ea88431e1c69b6cbb9aa3a65f22205d20e2a547e9758c2a96bcd9ec15dc97a9034dbb98615f5ab3796d3fbd7482d3676d60732dbc896039f660ecc27f3ab4a5fd07c752c7aa0df102d2c9b9f36610c3414f98f8e93e68c4d88ee811353412a69f0a495a98148ec4e03e80cfb9f835170556383b6440a4155fb321fa9a6b44fbb058e8bbd0b1aab7f28b6bfbab8309ff640b016212a4f3b36b42934bae5058a5f2915fddad0a6acf65153e2fc663e95ad6d7ba11dedab97e5552ddc9fde1e2285d459af653c396bdb8ba88f68eb0b93def5791b0b8dc2e8bd95f7e1fe9f166ee12fd0dde141d2f0830c6d737582dcc60737e96be23dd658cba87e8b34c342ba0da8032b13289c81051e7b4f0531aec243d8c1b32c9588efa441c56659ad130a76fb575a5b4f9d704efd3ad15ad0192f277cfabd10ba4942d0fb5dde7f7eec9b5cfcf9b028fbcf39cfeb0598507025fd687fffa26eedd9db5d6a255d27fd8a2b0e5e43635f6fa31df038334a62eacad097a001a05b15c28fea5304565c858bb30785cea9c85047f34502201105b690398563c902c4878fe66faa7b0496eeee923d61af7df6d9af9c755e26b94a884e383cbc6a7edd191f3c326fbba22e172e8fe0f61184e5fac1d1a5d9aa945652492c518ab4225ba435588a9539182bed95365387a9643a85d15d4b1238c569f42c2c36e1ecd7b98c858cd587dc302c22ebc7f3d7f382bbd9e4471571e55ca738ad45c6bb81a571b139ddd2c731d70845315232e5b41dabad0fc2a14107bdd5ebbabbffb56dc22c2708e88ae6ec651a3c1d467d45b7ca5400c1b64027db67a429a1c9f4ad786bef635bcebf9ece16dfc80af04df0f3414b4937648947a76195e621cc0d02519220ca486319a7d0154690429a90ba410bf36579d95271425378fb95c36e864da67c195d00da69da9fbab0b7b6c7bc585813cdefb44292d6a5ed4d2395911866cb9ba7d19a8716592f20f14ed64a1e6adf7a56d7b1cbfbd96e62bf688b943aaf1c1deb726c1c8f5a0107cf3800b10b57192516b89c155f19e62c5dcece4e964fff19c88cbfbcb058f2c1481d92a97c28fcdf4e2379bd685ef3d0e2a3c9c537bff80019f406279d22a7c278dbcbc3bde9e154e12d60cb8127810348f995ea8c6ae796900a9e516842e92329742b3a1e14b42285d42fe35885a07b50bfc057298313488f722e9d557f2dc4e27053e024c9d79060c7320769858adec21d1f66b5b40ffd4b3ef89c9c9f0a203c19856f5bf895ff54ed33573be63be3b3b28feeeb372558afd8a0e62e2570ecf8db2b73e3d8fc4b7d74365910dab697bc96f1897b46858b88e367d9418cf45d87fb877f67dc0c602f312ddf62b465308df6cf4e69f6d23919a1814e10263df23352d57927011df94c28c315926ced030c5ee8404926a71e2b96bda6897332c4f89a0c78fa81828e8f798d6279a41c8db852eecf2d2cacf2c409df992f6ec2b0de389fce5931c157e8de831c87f3dd7bd099a5cc8923c13e44a29bdb5d6a25dd169c3bbfff3fd2de373211b0b442df6c7384265d22619fe003f0dc8a69c20830690523db625a5f9dbce5656497a299136175ae16435b92e122e50baf831957b72f7d27029eed3b2b67ddda0cbd87c67be78d6f84ebfe78f2c797789e74bba2bec1aba771920fae7c91faab2a19fac59f862a62e9411b17e154d8f857f690ab5c34a60c2871e2807b26a3ec71e93f10d4b484d75549c43b4b8c8027d393e9eece6267e3d2d99c0aac389cca3b38dcff419cdc1b2419ba54e0392b9ec69acca7c9a4f36ff500622774e4dce28002a56d85123cdb5df6cc88f64a70d1d4b6e58e9667695466ca9557576d263b6db07cec6419cb3bf980cf4f8dde0772c2b33e3fcd3736bf96ec5b2cd308e5ac931853c944a42687ffc4741845b51f65a3491b012ab70b8f98b63534b596063b22a5c22dbc0227db71a3985b594429fba2adaaa94c18e3d6c9a41e2969e3e3a053ac1620386752d3420b2c5ad519c75ee293f3d47f137eec778ff7ad7b83aebd6fdd26d96c6ebf43825856cb79af621c6769b609d926d52a79e335c5d26f799f8b2dd28e95e9e0425182b6237401eaf393e9afc6384700fe0b94063fd3d0cac5c3eeb2ada8ef499b9bb371dbd1837c502ef34ced4d1d447d30a66cff39b5ed8556243cdcedee8eb73d6f39f06471c08b45b4f720fa1c235a17eaaa73260a75075d287f231a6a32bf5cd8ab2828b936a42fd2d8d62a06be45b059450e0d338dfd4959eb237834bf82b68440992cd2065f7ae8a2c4a84fbb56f4abbcf3996855bfe5d15f6da298e8bf7aed9144aaf0a48f59ed87a475318f094fa9a04ab46c18a43e345d2cce7e46c5af9196a7e91bfe36b6bde87e9ddea33e7e2de323bdaba615fb13e9f874f9ed2fbafe451f4f4f56873f65c36d7e9935cf04410d5c6c1b01ed70fa488eabe1e2c9268c9e83d1278de0980533c471506c4782992d44408867050478e5481bd862720f86f3d49b5e70a38b751c8db88a2e39fcebac90c73dc594baa5083bfa0e587d370e3c5f15d521f7958b75ce6af3d1e4a27996b9f47697c31306fd59d8f79f675679d5c01ccac4f82b55fccbabea4062f002d18be4da7ffb9802616d8499bc80c21bca384967f646f024954294b1180057412b9fb8effd5d4e5e077f3e1c841c9a4435c0d18673ee05c80a77157352bd0e677d1c74d04d6f57d4c75c797ac7712c3e3fc629728800c5602233fc47b68c18f744b60e4fd2e237191e84545d341f798c94a690e8c09b9ec6584e676b8fd2d567dce05394f02c7e5dab9eede0b3fb59fa97b181842d2fd6405d84d801ea8dd1d67ad936da90776b07a5adba92b7c4d98b81a69c1859894d8932dd69bbfd4271255fcebf51fe1aadc6022840496f1a788a3801e8d0b51ad2ae64039bcebe0f1ef02a3f5b6ca80846e2534c10df037bfe261701b75b6d749e2684c5b64174eb4e9c140d701234e8b0d7835c2420a22d953340932f1a60202d02d4f60b0b5404e949c74eb4742669b26aba74fc5a54dac463998958248b0bc45de5b2664d8e6da88936d3b1d0e4792197bfd09efeadfdfd9d9fdc69fbd62fb06fddf7adfb25537fd03d1fa7387db2b53622434b9c319559fca417079e31aff1d4a1b702fb457e64a1c68601cabce32b161d29fb1e5a49f3d558702dea382a0bfc8bd50ed9ec44ffa777ef6a43876bc8950ded57958a327759db62c8ea4152b7424aae87beede5914ecebddcf6bce5c093c581ebafbf1e271dea11fcaac56481d47f7fbdf6666d4acda3775d4f83a18d301282e59c8712343a3c7449e4625c5d0e00fde68c3f57ed5029b54d94ca11a49ab724ad06a36fa25026e5c1aac5dbb237b9c32b5d7f86c46d4f0175caf3d554f5de1a20663d3106b164ad1fe2178dffcdec1e684be97533007e8db4be71345c9f6fbeed65d8f6d2698c6dc023b960775b8e7b03efe6b6c63b3a91e13c9b5cef433b6386c8bf9a385c5d2101878b5994b25318c7621a6cb1f731d6c425222d8973d6b8269fd5924c1c828b5d1ca5db98071365bfffee91ed7579c62c7f4072e350ef3e2f075d6e66ab4b5b45d7a0baf291572be28ce3a7fa1dcefcf051b36a1e87bdc5d9fa12077dc17e747f06e80da3d10d3126fdeb80df4afbbf4c66c541b1a3f9d1fa2e80e99f7db0a74e6ee286a1c1152f7ca273258eaeae37f991dbf20db2e1a2f00c8cf5b4a991ccdcc6a9ba7f18e7e15f930b0fdca75f5baa7c8b4d0fbc9abf45eb6baa1dbe7976359d17d8c489e7ed0df6661b2e4b0e2c3f3f0e1af2a5d8294d752e59528aca00f6215676954543a40e61420eb9708c5153fe1450e458ac7a403aa88129ffe8b96fff58853c2c029993d9e0c6ae322713b6de75386f0199fc93280608b6a1f953c46d8b90fae93cdaeee06857ccc7a9162ed053d1b1645c3601c9ab155a4a502e39bd12b267797f36c6dc3c4fda3a9dbf549b9ba3b1753a7c9afc3460ba7c1b3acc1ba85a377a6f7a5ab4e9f47d14f86b34f407b0aff74830f61002bd6fc3f8a4edf4279da01efb435c7c57acb5b5b434e364bfd359dbc7d89863b6f87995a2e3053478e677b867c7d1619506fd7491454c8b38dca14756861bb82e73840784d46139c98b902c69400a7e49d5c0bcf1b293dd9d5fc61ebd886438c378eb111366072ee6c8e8f4911ae475e61300f657be67bc2ad2f2383926a0ca0bc7b0cbf40eb432320ddfb92cfdb3bad02abab650a73feded75edd02fe23814bfb77e27b2bbe712e9d79b654769e3990b1d85c92d832bafec8ab5dd9fde99b23d3fa51c60f1e62ae55e854003d4d19c5519d5b9943b0a4d3488d1c3eeb359aecc4ad33dd2dd188bed028674b29011821c8a4c68e76174b422762605aa5e4b4557637c54ee98a72c28850cba1eca94893fcaddc8d090b806294604fd6dce97b6cc3ed918f12cadb3a4b92afb486d71c0acb5fa5473651683df49c9d361cd9732bbde9f5e76e0f44aba18575d75d563b201da281bfe13cf7ffef37558d7c372f9d9712681c67083280fca908b9aee93a75126036a9910928391ec034b461803bc26cfc6cc30ce42934fc0d27780ff56cbc848bddb14cf40750e0bb74efe180806ffdf0019421c5298a729f5a7933e643607bd6f63d13137afbbaa465c31d721f79c1fdb5cba837eca49e7697e6ee5fe457eb7e024dc440b6fa059250ca9b4b5598148a0cde4dbb50eaa9c40aacfc993534cc2710a88330365ee0359273e8e3c78b948e29cb4938b650999b8e12395fc7c413cf29ac91c890d2b4343cf5768a3583d185bfbadfaf674f6df0f4226ea76357dc4b0cb22bafc821240a443b1e3908bbd488f1284938430ebd028a95ef469ef34bcca19c7584b63004aefd56df3a4252e7975523fa5b1fe1ef59244f11e3a9c2c4ede08e91b74b51234f8b4aba5a866554fea348d29315fa7ce76573f2a5dabdf15e7294f1e0a17c732fc38a760f4887e500fb623ac8a734e4344b02de685be302aace94958ecdd9d5cc7bc6550317d51ac4cf7d5778970b7ee9887d97903d5ec18fbf57618c4feede5adf6d166d88e5085d57058471158d908b3dbe4d1c64d0cdbc591e6e41c54a18e9d8b010653d56ec74a3c0809012e0d73ab4cec7779e36e35afbaab38636fb9aab39e4bb0a4304ed49409d328c139cf66a70f940f7fd286c90b16939d77c2c3af064dd6f035d30b2c7a2fae4c35b4cb49bd5e89dedb1802d5de0cacb4cba1b737c5227028940b2a199921abbe3a5e19631d77c73de72e59e439eed2931fa1ebfd17f0526ef61bf66733ecec2c4e76ce71e71633ca8364ab796684e8b69751f254f491aca49d2ab4056c39f004720011bd260a1c1d46afa06d14355f45d0499539b6a781c533b80d527553095306bcf878963140c7d5606d47332be011272bfbe1b543d2465fb5151eebd91953fc8de8a4225062ab444d6b39a62c253538c0d4f56e9fd2f42c3c60f55a1930ac25e46cbb75242b7525160c6b20f37fa7bd17123f7d38612b347ea0193ae92e68ce4617eba7f7a88fb7bd3c9a67549857f840ce6cf62face454584efe84bd4ba7e0a4dd092b9a110650ccee05c9ec46b14f11e248035eb5f23580dec285afef65efcf6b21fa97a7d3931f6241e2a716b3051f2f4a2d0c2ec6d27a4db69f27e915603a5adec0156d4eba4e292beb611c0eb6c9891f27d239ef5f12edcef8f1747a449b8fa6c7d3239df1b302fc39f2271d56d99f8374befae8e0e016daf076daf285f2a7e489764532c42c5ed9ebd54ffe287ca37cd20a0e9d25d378649a2878916c608d070a9563a090bbcc951aa225022cbb98cca74bf7c0fea890712846edb9fda74d1e76e728af17dbdc02332e677c63d7cb90bddd9f3eb0e2b288a04d2f5682fcd5ea7719289d33853232a75481a234a9cb2bc7b0702383ca2f91925b313582cabe7a1ea9543ca3bfc83bafdd5b8528e3903cfd1e75b32e1c5df84a5cae6f8030a1d157f6fd09b26e1d31aacc2a6c5aa23eb0382b3c199e093a6171a2c5251343a44b2662b971d2d4d9cbb170f5fca0ed292e9bc64b7c69c5905543a4a23eb7361dd296efe36a7fcda8eb9cab707e1c63b96cdf2e18592d32ee9dcf96df0ee51b6118cb306a71d1b70dd69c8b03eb072c9bb98a274e7b01d424e399d600c364964da6af83532e0d2865642462a0dd7df29466f66c826179691b4919f3aa64ec8e34bb7cf0c868dae3bdd5982a689ad76ab0129bd4cef029797188cff3f6c51f9fedcefe7e7b2e61773ad93d28ef9c52195cdbd1ca387e9685ffcd79763844e46540d6c10f828e97757a71e303a8597d379f94c7aa5f42a6e8183cd2690f31cb7647dfdcc80c2597cbdf96aa817a22bedd96d6324f5bf018b6a7d77ec27eb715fade859df795b4ee9c3f92373d54addbe396034f2a07f2c6976e166271a2c4a57bea867643f5887ae5a8d34d0a5bd0fd19e70ef54bbb51d8c9ce9cd2ed59f43484d03fd4af08962ed6dc5336cedefa4697d0118f5076369e8f8661286b1bf8275d04d356a256d3fb52eda7acc5920142ca155650a94d1ae916f162c194096afab395dfb0fab697063c1a9e658c7b18e8de1e3bacb1e4f85ada8b53db5e5ad1877d2563c7f3ac993abcfffefb7f7f0c34ce4a31ef56e70316e959353cc69cbe69409d20e4cd701563cfec200cc8c36949c2ea86c309dc6620278b0f719be1efe2987f2b9ee27bb063cd79dcbdc080fdb68ccfc46f5d3290203907ab42ab7bb9c44aca8b6398c27bd0136b28ac22fbd6969eca3e73573f5825d749ef4107fca17e7b7bcb6be8e33730157d2f17153fbf7b32fb4d268cd743f70ba52d7b34f50924d24f85c00613e256db2142e5565f2272e225a7e78bdf79941ecb080a984f2d82e47b2b95093c0e11f99c713c6e6387e4f785e4da01368413c50edf6ce355a077177235c8062bf9211fd68a91f02d36c7d7ac6f7bd9ee4fdfe4d2d33f7df7dd1f790152e20769909ff600a862854c29511529798cee45664140de84660f3bf1889b6965975f937844d4588885960e9f4e31282b1706d06ab5994476ddb1917c3d5c8983f5c3944cab94fbc45b5b75aed5b9b4394e9878fc695bd09354db9caf5a45175ef8b6505d0da9c05a594bd17ef5ace396970e2ccaadab67b41c3e3cc6388c29adb34721ee64fd1c3671f5d1b5c119dfc5775fff825debae368fdfde090f279e605b7e81d6bd82d5a45b6d43681bb1f7b4cbe655bbe90f7c35c7c6d6362472b4113991936294714c8a18e3066e28082f744954809ec5ada43be894c818d68093d7e84b5cafdc8bbb9cd352d250076ce516c99697b40fce450eaa2d36274de2008e2f2ff8c98383836be8d3d5d2b0e9f9f930e9b24443a8e36b4efd1183efc60dc9cab920b18b14b53b8e95083af9d288a31fb9219e734769655b99c8d36c7a176f7bf8b759f6f7d0c330b6652e7da8f4e030623e5a4debc8a7cf8f6625ed74e92d64cb81279403bc90591d5679ebd8d40a889aacfaa8efe875502a1e9d8afa4561a263c90657a7bb6842afeb5888163dabb2a6e8a73b274c9968f55856635238d207c7850870246d8934cd44dac0592a2673803690e02756f46d571e70d71eb4665749ca0ab07c1a1572266e25f629495d3260026a7ffaf20467493ad80056d27959d966185fac3fda0b752ef7e7bfd71daf316156b8afb7a3763c9d86339ab2f4cf0ed92f13849cda211da64418004c638f496c48934356effef174baf38dd39d9dec9f6e1e6411c29b6425e39d2b8255b3d4422f19c4a1e9181974d0fd65af4b33a03afef89dc7c36b2df7276baf55ec4ee9598e69515d1d71663f8e84fe2556f65f85d17e0975f395d16a4d3a655beca320ae503c3b392a66f222cd14c798694e5ef0745e564d2d250a8026a289254ea15c10915f3c17dfab4fe9f3077b01dc06f16fa74feda2a728cb07dd73b7061fb7fdc1e6b475a0ecd9e76188b509666ddb4b91d1616f5a5680edb697c698cbe4349feffd39852f0eb5ab150a9ac28af5cd5fd3f3486e975b50dcf2064256c86304492a6e11544e71f9a1a59ee38f9531d789d3925a6e32b95af41ec6cf4874d8f8ccd690efc505bbbe0c0c39b46b5845855a56b4a16b101e4794ed0cbd41a6f9c70913493db10fe099d68127ce001040efbf5a2b50cbeb64ebf853871fd8215fdca0c1a0b88210d0c9033165fc4c7d51b52dc1f07c274bba6f19f6b8608cea2f4550bfc1b30ba039e7b50d06fd5447b94ae6e6def46e80df0ad51b89fbb5e4303617088c9db5c95363e913c9d801816177b5519b728a94b5000040004944415461b01f4b6c92e95ab1aaf20dbd262dfae95feeb0c4c6230db28e5fbd09c184c1ca6aeace1d18e2c2629f3cf642aed210f7cfe65b8c9624ee78010ed0784661367919171bbfec08641c188fec2d67bc78534e3ae158baca9e7183448643faaea293c8ea379d0d7dbe9fd5b7d948c73f57dd939731b40d95cea40cddb488f6649cbd406832470dbfe31d911a57220c635d68f6f1f4d91eb6bde0a31f714336842e7118dff2eeabea9740dd82b71cf8b47100ddbb3a7e0aaa961babd68cfe96c3aa7e0bc7de94e26a66083e8c099c98b623b152fb664180a8669d20faa42d691a1ffb0204ba528046740f08e9a20bd5d817cb89d94b8adb4c0a916665527f99a3c275353e2d17d776c409adbee44e6f3a659ef836c0b6566f5a8b1a8cad89a3004ead74168c6d2fbc3b9d802f159feb18c773857e7a8ffaa60d7834cefaecf0fec353abe93e30389fccaecfe0d0f20c46711ec6d1143bc69f9dccebb9d249323c8b6786c12409cd3b597711f9e6d96c976d36711d0b6798c4347e7b98d5f9bfa48edb061a403390124b90385453071fc9e8614cb2d174bdc337b8d4cdc8dc8a0823371d74bf26ba7fb87f9caf8a264e9a27f6e3b092c6f0ffd7f4e196f4390252e22933d22a058e0ee2c867f21b262dea975fe2d46bcc8a3572247c0c6fcc14a768c9c281c961827d15c1fc74bad84ccaff56f836a6afd78277faeacfc9a5f1c60b98aca65b0ce9aa6d40a716d1c9bc4f8caca65f7389d5f4b32eee52687b785a7200d9f8b3253f3a6e656e93d67951ac02423223df804ae4d61cc05caa778146cf5d05a967224a165dad50b6a5d1344407706dc5bcbb3867310987f52b7019b97b55ed8946d81ec897f3a4c8d3303d501b48dced1f6eb7498dc062e8d58b186b7134dab653374f4294b5f8f027a8b7169d203ffb9571fe07475c9838fc241ddf2ece23f5fa65e4c9e407010f4fc11227b0e85086ae92d5f1aca2e3dc1d2f76cac80f57cc8555c719cfebcc17af8077efd61664bcec8a951b7221447b8039c1d8e7ea438dab0c1335f3a06d7770cbe0700e8124ed7551af7c792caf5351ab2ffb45c14af1100441df36935f95b60d3566e4f10fb72542fb6ca0b5b42113e64555da54fc543858b8f95c587b10079b0810eaf38248e73bf9a1655969169c7e25eed8eac45b4cb79ce0f892a77ce67e4dab4f4e88887f4f3ef2d09cfbba60232d3df3d286197742a6bfd9b6d17bc771edb58c3e6b60558603ff0e3215c5983a6fb83f1dfaa7b6bd3c9ac9b9a86f8f5b0e3c791c4077ebaba4287839ae2a5cd5e74907bb74b0d958145dfb1e3d8fdf03127a560b21e2c48094bd881e69191acdd8a51617daf4adaeed2d89a5d206a55e3519585515bb949c3250a47b3de82c51e7a6d46369da2cad3266c24905bf70a469d08c0ad6a254d9d4182079f85493d51d520b9c116ac1b35ecb6876b6bdb0ef653e3fd03e0cdb5ece5a4d7f3417ecb3ee70b9423a6e078ee59f6ccde64447e94b3ae839dc8389f2ca59c280e12e06c8a855c72b7bfa6e6eedfe0decd6ed35779553ae635ea1ce595972757cbaf83e787b28e5da5a43c406b47684b1644e2727bb29d9c980e68ab2748a6e774631b76c77113676d259252e67dcaf89ee1ff245d1fd131c76e2f9baa85f184d1aa1b8a0b34e5f6ee95789d557facc60db9ea4d360d3692a5d2053c31f96c04365d65cd31eec92a02e74492513903cf727469da5434d966801f86472db74327f2d80b5bdb122d4e402c747fc71ab8b6f7b69af9ecc5620f0866d2fb59a5e0f463cd443a4ad01dbd365c2013e2af14511bcdede2e7bca9372a95155e6ba7829d0caa5c614c10d1861d789ad2c705108e5b4162c1a1d05be908b9ef67614c6b2c8a52039b54f1d3a5721db3fe06a711e3e8248d9199b605bc8952ee738edd2049c43d311dbea2fc9288b7ad3d0c074e5b5cc7fe139b9e43202b87d4aae3aa7ce0ae3cff2b647272f131679895b3013d1f417e7cb657bc72edb5c6c1261b9dc3dc6e8542247b6b8a087beca6b0424baaf31bfb8b7b7b888033ffcf860c3bfe13b0fafa6da1ba9e8419a411b698f8539c40ed86882fd92395959a6c1015716788e1d7f02c5b164c3d13ec94b80922484aa494bb4b3e201a6cc07a9ea0d72e8595cecc025d218ae136c3069392f9c3c87870dcfdbd97256a4f0bc502a2d0c60ad9813b706da204c4f5b67bac68338f472a1429663e3c596e574c8ab52db2ebdba1bb2f44955fed3740b5bca1365e3ac276deeecb7841a560fe157ba1f8167350d19cdfcb2f64046471a9d9d9cb7cefa8821dbe853cc819d3f9d556b75b1d4242a138baf0aa018daf7e80ac780382cda9c906f1c048a6ec6bf01577c11f979aa49a56c8b19da8cb223d00339ba1744f39aaec7d649401c69481cc222e7bf74d51aac2f4441acc524d2d60c6ec8a642680baee2c6c836414f8324a4ca08c682aced4d37f7ac50db5ef0b18e8fe7dd0664ebcb59c80df6586c404cd925b6316489a06e95d301fb50dd4e87ed3bff31e276cc498f53185191620406fe2dac587cc7747797d5deee313a57f5f9cafde575fbd7e206be48753b13c83f955e19e8aa2b12130c726cc064fad9a1d248b9629cec7670355dc7b4ef491f3be959456f0ef9b88c7bb2cffa5dbc78f1febda383bf8260dd92fe56e753b4ae02111cb851c221a7641013409c20e3c09ac058a89c0fbb11aead9a40524e56bf29633d1627920936822c4cac25771ea638e9eb0fb049ccd754e6366d63b517307e6dd5bd2ef07b78a856dc315f4c1be4c1351baf64ec1775fd5c98dbe365c281172b9bf929730a56fb45045dd62008f210f9c53a44cd9034802438162010f103272f6634d62404009b4881600d87aef6004e4e76e2c9a2a7b84d7cc997af24ab307950a94a537fb5a5260e742c6d822eedb055281d71623a6ee98879f5cbca6874ce96b495e7c4282b4a3653a8b724f8092b829ca8b756e1a900da3a94dd110e9ef52da67752ee26ec8b052b185f4b2611bb14e7bda179bb149dbec86fcd667532cbe5dec91227903b90ff0c8ebe82e6bd37170f6923edb1111912da9d7ed34c56b8fc930fdec1d3097734404e0fd347215c7099a36d72bccdad552df3c84921a3952eb3c618433f9fec06ee446d49e5a1d0d2b0a41d1349844f80e5b03c14e0a889699e5b5b7c0526b9c1c93890d157b675e3f5e02dea6fc76d47c68b027162a0645b4bf048eb9d831c5ef9702805ac417babecb8a0ef8e1adfad5ece3ee7acf257be15a4bed9ceed34ebf41d4a809b7741fa047de525bcf4f12d6f9df447b3926675dbb0e5c093c881cf515763c1dbc24bd40e606c0c79bee2b05e68a14d2423faa8d65a4efdc206c41c108bf100075a8520cc5fd984282c19b140d0b16c51314e0afd0ba63646887a2a7dd1c822c69f7845435832020317447143c53659ce45e48234d285e0b6406d4380d2133705785df9f4f8d74db55d2dd03cbdedc5db67daf1bed814f4ec78c961cdae8f6d80788fd60684db3a5e9b2bea7cca13479d9e32301ac6b0c78ef09349190838625edd01b1a7c63b92cd59be95c39b8dad1c73e33aecfeea2150219b011ebe99d5eb778477902ef970101cc06a04ccbf81397e4f075d47541a3afd9d960e698f8fcffbaca4bb7a2e6cd3291fe31977eb47ffb1dbd611f85afa7f6baefc6854e40aa02cb1ad4e8efe985ee013ed95c35e92ca4b0a46186415fd88b079ae82c117b368dac7eaaf0061aee6d7482876539c74de9873a99574b8e0c67d99216feac15abeb6ca958b7cf102a6bf7a1294845a4d6faf646c30f8a3b84fba737e4a4e1adef6f4f4e50046e26a04ee06b731287fca9841b13b1594dffc296f4a1b0139ecb0247bc910c302b087314e9fa822a482a6a75e33ce16fd0adddcf5c04378387d7f11d1fe3acbaa0b5d8772611ac10716c20d81783d5c64552a463b1389d3064c674ba5a96d0d4651c4aca2d68a6b65d32f10d1b2acc256bbc1d5d1e3a763671b74e8d32662a92f74d1abe9f407994872276bcd579f2ceb22394d38e16151dbdb0d3d77edf6f72ff2b04eec0fba399f1d1dcd8ff8e4b417ccfe74fcea57fb9ef904db47e8d937721bfa2d6937ad7095bfec8b8cb107cdcc98b2dd81d85add5dfaa82da27026c8208b0380ff7966d92ae1a86510ca5c3136ad5c0c572859309549ab2e12002800f0b83e2e44d471cbec108ed5b88497a473b565cb1810dbd6f86df98c53f84bbc0620b4fa98c43917dff134644f7ad5619dc2cd7154e3a49b06bee32abc9d752f8ce52dda4990524ed25e659e7cf8356c095d1b5b5ec5e81d49c794bb23a7e717bc75b7bd907d6678b413f49944b6c02d079e200ea0577f26ce353aa2e150b5d57293b11600b4c92c9f549efa111807f5cd3cf42505c8b098307d1ecfd2a9c516334800333bc648839478d918937848054a5c9b2476e1e50852a500db1673ab812917dc8e4003c4d07637c4464a04e0eab919ad1fda8594584ede81765f5287ad4827bd8276dc4f114de7d9f6c243a46ddb8b77e1e69ffce4279f90373e3553558ed878657d3977ff7731bb38534cc9dc1cc690477f65abe7acd2f83114d30019a79b71b6f960c9ca798e079d7bdf7d159d6d2ea3500eb74ef7dcd5a60b94bf117a6fcb4064d4416e03e5c423b311a0174b42073d0f959a20b89a5eb1e40d5b3b3accb34ebae7ee889f75367f235ca4d25750f3adb221034eaffb80e7cb81745ee1cf0a5f9c747ac144207a64c63c19e5151de73824c09c2fa5997da19e2d961a843bb14117207f975c49073d93485d071dc74917560fd6b29a2e5fb80cdcbccb3076d2b39ace058a4efaf8034763f990e6365c1e1c383e7ef085b4f4a074a64cae06490396d7a43625563e879f82684ab944084551f795ebc8a487082f40e944b08521d3221394f13c30346862c1c7c7c5dee21c45ff076b938486036ad495c2856a3d58aa5cf4aa034063b89af52aa7101cf0d48fe89648f6413a96e75f02d9b32c828e998e20a10c76d1cc8aac79d1576805177de56c541dd4a96375fd1ddc257ccff82d36cda1c3a6e9c6f56b93bdc972f5d61739116ec441c7d04da78771cad1c70bd8ac237fc635fc345b271ec7fd48b5bd389b2fdf48f5afa5970f66fc6c3b7ff2bedc50fb0bcc9f70c78e91333fac6000c386c68f6ce36b304b3856e65b3e65328e50e22c25694adf1fd813377b48377c4f391304792e120c8b634d52fcb451d6668c0442c338d8bad7e1af2becc568f21d2f7eb6b78d6377d005e9807767dcd576c71370f06b9b4b4b03039a712ed2d428be1760adaed022eec581a57667edfde9dc10f19df752a81b25fb54d3178330a42d386e7cd2c85be0c1ede37bd6ded45e667bde72e029e7c07cfe39ea4d020e48622875541f9d8b93abee80a00a6a17d4e556a09d2adf84b914caafec4e0c84ea4726545b5526d57f73cba729bab15352697e930b01a5fa9c414ecd44b451a92d30eb63de69f99507aca5d3a4c22e7b65ef2ccf2fcd829e6d0dbd5a1eff45122dec52a62fb20474e233466a3e9b13b0e507d9f6929cd56a7a2b5ba7f16afa63bda3d6d856043756d6f73b933279da0f7ae5ad0fa34ef776d29819355490337fcaf690c5f20dc96e079d68a72eb7648c1dea8ea3736edcbc71fe7cbef887cc1faf836826a75ac98f18953c2ca77f668cdfe9f5d5741dd20ef3ccfef363f7a07798ce79e038a53aa65c019d3cd44fdc83a3835d0cfd2f0e438b00964097b0459033b980912d41f024fcb3dd447018e8937c4ad2d5317959fce4e8a89027873349e1d05b3c2426cb5bc07f0dc9537bd281651271a6f0c764d1785a138b5f5af57df25e0ec29f4b5ebc749e7427bdafa66fc887d56dc365c0013e30f392b26265e02267086064d0db9dca9bf23808993a5e1a9d57016a34957125541ccf869411b7415af9a007a5689f8cb682a4dce88011bc115b71bda05892d6964251936cb3155987a9d2b5b23fd59e54a52ed9c252b4e88dce574d0cba5ec4a36f9ea159a60a7b461ef038df3a69ad08278a7ae44c7e1c44332de86bfb168b9b92391c76c1c963db0304ba2e38a08a7b5c1bc7e6441f79916c5b319ff24aa69da3e3387943b144762e5e3cdab93839e22b7447d4ea8abb1efb7c77befc6538f00a9ac45b9e8a23f2460be198f96f4c88bc70fe897d31abd9eebc81c13cb05c00c8d9898ebf94d4338f70d814299b272e29f29cd4cc760c92911365450a3fe51d7f2249315747f2b0e0556339e43acaf236b690b37f1904905d9dcf0a7d1ce896c691b74c1efaed0e86846dbdd5591cb820653763279e636763c9a88752cd3759b291b6b7fc1099cc7ebbef62527c7311d697d635ae5ca131b6194fc6796d8eb9f65af2cf088f656fea1964b6a02d079e380ecc766e886d8ff155ffd59146be9d9d1fd40f4ef9050780b623eaad61f05f05f4c77fc157cd94947676656fa42254e2fc879e38f52b27bfec8badc2054a889f2512b09cda595a96490eedb10995ef2e06eb05c2c4242d2c4fb53558228674f2d8def74ba4d8f236b88895393aeaa46745fd50fdaf67acf68ef78e7c2be3e643a4a362893ed63b6af27e087dc554878c8eed557765099d63b53ccc043b4cc0f0394ecef26102f19a1866ef26fddd03d151c455ef5132511d747f9b0eba2be2fd07bdb751e77fca84fe8b54c35b16f80bf37566177f7693a6e5c6300c77f82aeceafb4ebfd1a4af1c77c7745c761cbffaea8317226c3f747870f88793c5fc8d112ab9412457801104da9481a765e18f131af90a116dae89c1d71b292e047072abd53cfe9c14bdf094a7a64c877e684ede82607fcfa239e0e3b6d53622e6fd4c2260cd9b93ce3bd3152c97e4bc78f1e3469b172f79db0d0fceba92de69ca13e3dd49ef5b5e7abae36dcf97010716937f479d8d33a4c03599355a5b094bd39547650ee10b9651f724f311b2f2730020a684082868e5dc49afe81b53c641224fdb08ea299d9782815d0bff21ced45fcec390148993ad45a6128b4acd00a58a0a97347f5585225a385a820075e4d4c7669fc4f3f57e964bff80e78d2264e8d091485f8367dbad50e70d80fc2a37aee0e6a5c864f9fd946d17cabc88b12f9e93dfa3943fd91badc44c0f8e62932837c779cc6dd2bd633e8c310aaec6f6e0eaec032d4f679da8bfb9ce3ae70fcd7616af847737cfd8769436d256e74879d839424aaea41f1dda6db8235779ed9227e5b43df4b2b13b04459206f0f03ec4e599118e96a388bc49fd22c751a63d9ce5b18b3b8e479cf02a550e347347eca07c8680bc750cfdf31d3d21dae8e970e719017345e4a72dcd9d8eacc22b13d66563ccaff18d53deeace6b1a89a7ad8eb12d0a3a71e1e2553b6e63fc18dfb8e86d297d74b5391a70f0069b69530de38dede3d534f31eeb446dd96dd872e089e4c0affccaaffc2914f8408dd03e68afe3c35189baaca2c43ae6aabcd90b71d4e75cd86b5b2c295e30d3bc8a735417cd1347a40473091a0d63da7be9918a6602323f6adcdbd5d2631a800a51ecfc8fce3124e657bf44d524a44aed8859a2637f5af5853b9ff84adc1f13bfdf3fc3be8de7afd8700d7156d3d973e29748b501b598fec4be3b3dcd68079abf0ae315530cdc5e0c70718c4ec146189a2003e86906150e87c11af2c9f476f65e7f5b5f1d5f513e3b56787e2d73b505a639d917752afbcfd2dc6eb88bb72b7c1bb734bf12b9790b953ee80063e83faf3bf4fd6c39cbe8901a8cf7305e4d17d69df49eef593ef41f340faebcf2dc579f3f7fc5ff315fcc7e1fc7f935d4fd2c7e04f9914813028553a1f3279c7c4fe19b7942980c151660ca7ff81a32c2995f2cedd972d20197286fc061e5717af2bf0aad30961f6670dec31c389ec2e0a47361e49697eea49bdf79c204aabc2170abfdfaa65d4d3f8b27e66dc3e5c90164e8cfe99474272e062f4e762414e52a99d5f18e6c2a97e9aaf25789ac4a000b0df59f78c78d84568114ccbbbb83535a70d6e204f6e30aaa7b7d11a25eed09b2bed081a24e0d5353816a4912ba972c0c988391159f43f23d5a26dbcdc010a7da455bc18de3a73366e745a47d55b4d1a0cee81af557dde158b65e6433b365dd533d9dbe95f6bda72faea6720e946dfab7673cf6476bce7f4bf3e14d9df4191f40de63f5c5e51702aff53b6ebf0b579df857e178ff7878456c10eba0ce7a818055db79002e7d073d7d1d351cd2492b8aedc818c910fb9481f200401eeb9e6b601c6b39046c182fe420f6a93beb166ba1e096b0a83c0557e2fc0fc3e00449c2114addd64081f055ae92c84245f38603c2c976162ddefb5616d14c334cd26a0f90fafe7a37ab743828e5a40be017d73e4653e79d8b40e1699be359305b9f7df12088ca3f07ea1aea243fc0d5db5e44e99336911a5722f43fef4f37771c1cbf4b2ca84fb66f7b19736a1b7f1a70e04f6b0a6203d40de2b92e47af31999a88e850294a8bb746d703ffa5ffa57f511c72d545e787b215514fd00c3151651a0ac0b1e7a72aeb6d95899be0595b4d4efc6fcd97300a7a76a6c8dcc13976cdac74a2ec9326ca3a04a555b17bdaa5d646fbcf4ffb8629f819547c7c9d4da9f5b0e0ad5d2e7cba6c72c8b617830b2ee7cfe3aa97491f6c8479e30bf5c773478ddead425f511742e735a1463cc20698150608a5a36152c54df33be4cd2fdf45392612de87de56742d7b56d049af55f45a651f3be81d5fa772f347de47a9e38d6c89f95206e6efc1e0df63d6d2235f73ec3b8df1d987484d8f578ec7abe87db5f8231ff9c817dff1913bbee38e3bee782793e8ffcb0bffdf4e4f5f5682400c9e6445bf0955eb3f60f951a9b0ca04935f562d8b8d003acf5a5411495e2fdb781b062b4cd34f7078adef9797dc2ad4da1dfdce0753faba65e7bb7071e3a4b3e54587e1a19c7479b2e9a48f2fdc3a6f56f56f63970b0770c9fe5d4d60a932c2a69763201a18a2a7db96956d0075f168ba8c612413795ce9bc52cc5f0a23bf71f44d038f2c73a650f0d9675d95ad8e5c74b30830fdfb90f8ccd2a97296622c6947da026dddf21c4b35d2aeace2db6effacdfd9445cfa246e19e4b4aeda68597ebe95caa0132820469f98ce6060c035de062729776b240928b6cfe36cf95e56e7df528edb6a85753addaf0faea5f4b0aa1efd3b3e9e9e40f76487ed2e3aea78e9b94016758e93ce49c35ec61d0fef5e70cc6bfe7a4e3e3fe24f780bd2c84ace6cf7e4a7f19bbf9ed6de55ad870f363c3ce2ec7f4f639fc2330646984e3bb18c63f6a7074651b7b7646c4b2ef28200e9c1eb7a085e5e561d03ed40aa3e1ecd251b26c2e4aaa7ceaeae67e51ae6daa472c875a27512ea81ddc4e3450bd7a1ae077d1daf5af516d6c62c9d236d1f42033a3e21eac4ee78c6c977ac2bed0790ea4b89954e7bc0abfa1d6702651863de9f5e019c31df6dd3903ea8bbde3e0f9531f56ec8c431bcd70bb2d9a987c8b6abe99dabdbf3d38203b3c90b6d473ec0883e46cf49f7bb6a80a2e6680bfa54364390b6a51bfae0c4f9d53e74fb62317344f458f12cfa108d7d056a6e5c2622450780938639fcc7966b280c2101c43338355f996161e789821bcb0a3ee75e94861549cae9d867fb760a58c6acf4ed6eccdedb5617e56bfeb6d42ba0e92a7b5ff0c5be0df35bf7b9ce7a88b45fa43f561b10dbd4dbb0e68c2d26fbf625ec849b32a6bea809d4ce692c1369c3309bfcf3e5c9f2fd450b5bb6be5db357e152d0b0d5a503fdf08e70d3dd3187012c3d5560d2b8d0e39ecd03ef018ce1bfe0d6c577508cbbc3eb61d3294deee81b897d1fb6701ef8f9ecf3e7cf7dfd1d1ffee0cf9c3f7ffe4e1c89dfa2c36fa4a72f23fbbc738ea3eec0eb9cd4de7d725d8192498472d249b4fcc62445203cf41c1924e57f0280d0a4b2e429f04ab125f43fa6cb0f4d9627af667618bd787f9827e4e590d06df7e2275b60d8ee227d2f7ec0e12d053475517717e0592695f14a7a77d22dd32f5cba2c8c9d75f3b7e1f2e2c0873ffce1cf62e7cab3cb1829bf4a6619bc7a7850795436757714ccfaf598bd2d03dc0ca7b2191620b371c64840b4eb013b31424f94c8bc9104d5fb8432d35df64cbf947dd2ff79eab06c8c65e1a79dad8427f5a32e64cdaf9a834334461e274c5ba41f6edff4f174f0432dcad5f5b63b74f6bf51b2fd29537b9e5399cea4e5201e07afb58142f760c85fcf239e8305f7ade9dd5d57fffc51f684d2d13fe3e7daea3aaf73d17445f740ebba1b5a1a767ff7de7b6f8cbe4e5eff89db9df6339c751645cecdc9ff1d2afe6a26da5b334af2c68ef9cfc0d4e50b09bba53baa6d31330062f4d774e1326a55b45d80d9020a02942d1efa589b63561c658f3ac4f24df494b11e62f8ed3ad75e2c65953c657c7562c1836b5ebc71ab7032ad7170a51c8af52b6f3df98141b3688063dd5e1fd0a41a37d6ce8501b00df9e7e44abbc30e907e9313c1696dcfd84f0ed9ee759b5d733cc761b4e3853b25aea8d7db1e069cab8c5d6a3d7db25d511f18b58d3c1d38801a7c8eba5ecaa0de95bea46d4dc9bb2d3089d9506d348d1588acd2da91681a888146eff463d4c128a605f9d50a3e20d0e24bd190ba1090acf9eaa331d0b5551efc97ac817c2d4261704c5bf59db4625547ae06821f2a16e299197125d470a440320b538bc98ff1c14aef561234cb673f446aaedb5ebc48f75923d3aea467eb4b12abc313b59a2ec53ecfaca8f7d88c8798642c86cc3f43182c036536e90e27c2c355cb1f08120757ca77163b7b931db8300a4c552733acdfe6561736a764c2eaceb545ba43d98b6fa699f4720782321d65edecacb80620e1f6745ea9f5dc73e7f65e727878e10ba8e58bae3c7fc5172fe6f367bbefb1cb813d53ba2286110227275399e6c88358e38da8856224ffc92fe10120aa052c23420b0a95c1930a627ac5559067cb5b10bd7fb43bd94378ba3feed45171bad781a1c3cc0e0f5977174ecfbb936ee6a6938e63ce9758c980836327dd8768c5ef4e7abfc3d2d3e66dc3e5c5819dfd9d7f6f39476e913325ceebc03c4f112b092072a91ceade2a8705cba92534b491532828f691dd062c74ca412fe81cc4291b616e175ef534e2750517fcdf65712bf70d47aee046df822e2df3cae8e70258644d8970709a66120f628066ebc769d9f3602878922b0c4a3412299dca2d21b11d8ead0d24f1f55caa2e67ce99a17975bc7bfb0ddc9dfab764f3e2ae9366b0c866cb8b2ff23ac1ad3b99f6d7f2699a8eb461e930bab8668e9aee26afafbe48378eaa9133028b073b3aec38ebc9e56530dd8f3c3a8726b357d05bb6afe6f72dfcbe998e1fd82f6d0b0f19d0d532875ea46157d275cffed5c0158b5b56263479e7dd12f3eb628d12ac8c481268e6833038fc8267e0d5c59b7612247817dfda02c62d55809cb457667908987416e319579d6ecb845e9044731ccb6d77655c271d0868d5cf4cdf0203e3943688279dd63660a601a45cda29aa8ac13f191f9aee2f2e14733359096c41689f56dccae4eb33adaf42fc74a3d7f1fbe4fa6d6fc18f7535cdb2dbb0e5c013cd0174fa73435315573f22cb24d40de3818f243cfaddefc359a6f2d400670f1f5a974ae0d223ede26ed917e01224989ff92879d8611d678369d135f63aeb96101738a4920f591c760e16f197cab4e5b4583ceb485f5a6dda03b3a54634bfd875d17afb26b72da7f35f2a773866197ccf1aa13382aff6c653e73ea90f051e1d1f9fe79a3dfed8da97482f5cb8109def6f7a793cfa1fd3744653bcfab828c39c43c304b922234dd3c17234abe7c4bf1be8faaa766c5c5b3d6f0f8cba4471da495fada43b9be9906f3ae5b66033743cea5e5b6d9786417c71885ec524f7df2c17f39fdc3fd8fd038cf407b8b5f3d3dce6ffef18b497d1159c74fb51039708659505fe39148b14810802400525b771445072dac9a8821a273df0942adcc24a7eb618f432f2d5cae4ad6527532fce6e643de87b58af6b4eba13843f837bfad79d74fa8954d52b2fc5183be9fde267cc536f2c78ebe69138e9d2db86cb97036ca4f80225b4ee80693c49286a9135452f89a415c338478990c85979074c5ccca050a6ec42a327ad865bc44beeb58a0398b21596dfceadc7cfec76a46cb47599ab615eb5d1b2692f36c76c8d7be8d310cd7054a657c0d9978b89c257582957adb57fe2555174d934faee5f6815d1a46b2596098794bf11dedbe6cbdd77597b828b03fc630fb858aeb870575c03c397e61c8bef97c574d4d942c73e46165fe6e7bdc04eded849f7357e18f693fe33ed0fbd1d56d78daf56d76b1b5fda73ee9c2bf5da3cceb31fa6677f1bbe1dda07df83aed1961db5175ddb5dfc2b66395600c8cf18e2888bd7e74ef3cad63b08f0443a92e410bba9139c2b24f95571ed64e29ee5245eb7b564bf39349c782514079a7cd38527ccad27665b0e78f0b0694903931e083bca0930b7bab8c545dadad5eec0c307f22d675ba83b6d10d7baa45b75b08a963a8243b790cddbcad2b6a3277e8cad7727db6bd97cdb038119ef1c5749ab7055e0fae99ba1dffade846fd35b0e3c551cc03abc0035401b0c758c8e47bf85355b8c3dadf94348d9e1d8ffd896b227f1875282325594a3da1aeb1258d505054d89b684a897f4da2611cace00345d46a66c52a791a2da1d50e2876a55b4f7fa6814b21c1075db56341269bbed303be65e540b02a9f3f80d5ed8736c3a3bbfc70ba1f54a46ec3aff71d27d88540a6e4bcffe741397083ae88f57ff6dd599818179201da62731e0f4dac1323828b5452313c0cdf0e05dc29b83189ac7388e4e5cf556593ebcc35618a69ca1f3b5dd6572d21d497debb133e97ef27a3f8b94097897570310c6a45756341930bf6de768c9b5136dba1fda3fc5fec4fb70b09fcb107ea623664f140c07ddd5c4080eb01238736b62ca24d724c9b1ad4906a780996cd8eb243ffce80b67454667034c09d749818049546305890785785f8934cda4fa5ed6c45f3f9bee65e50e6cc25a572fe1a4af2e801ece49cf9697e5ba938ec36ecb4eada40bdb86cb9b03c8d497a4075d9c91e592cfd2019fde5fad5e20f7ea8205725845cacc0927036135ad33bcd00952b623d7956d59dde6d4203eb8da37f6917fc974be7c79e15246bd829666253a289ecd013b7a927aaab4b0c03944ad9267596aaf02942522c9865b78e043337a0e85d2772b200457e78d3e37cf34db33c0afd647c76f9befecfec8deac3624973dabb59758eaa2443bd8f272520f941ac726c569d34967a1a06d7939cf1acd7d83937eeedcb990d0211fdf2695e428bd78ee739f3bbbfffefb5b4db42dabeb5770a1cd753c81d59cbd738bc99157f8d4bb3fddd979e7f478f1f5b0f7cd7c3df5d956125be7399d864fdc65b19371e445b0c3e4ca86f053390158bc2ca0346a0e5046b463f04e969b8db9d3a936d47615d3e407e238d51ef472e05dfb0e22bc874a79e6292f3cceb4f2e4cf3fcfe0114939c7300f8ff214953214473ff8d2b5413af0e2d8304d70950545123974c722ab80d46fbdf689e86d31daa469170b663067d8f3423c37347c880cc278e9c7c7bc488be003c19eebe2abf6a7f71535e18f6735cdf2dbb0e5c013cd01f499b7d84592d590042dbb4aa2cf175b0174b0a3ea0f50cdacea285c1d37a49cfaa35d168110da3900639ec9e289e9645abef04235f30ef5361b5e38d6662e3f224e17491837c73434aa0d556f2c8459ce6be4074f349bd96c5cec14f356e6053f5e39d9790fb90f1bfc48a45bde9c09580098e7ad5d78ea67bd927164bf43f7f1ea7f71f98c26d2193e9e178e84597d22ee06ae5ecf33fb049d7f9dce769168f31189ee946be35808e2fa63eca4bb777a6770d23188c32abadb32f263c597dbbcab1fb71b5c05167698379514de194d3f056215ea938cd19b19b9afe0b566ff3e22f38f69f7dd31ce4e480c682603ce999a10a692a1ca5360e14426811a6b04aa0982d290ad04104b1e98fdb67f64d8826428140a0effa1e62da1e61239d11ce23cfd4fac1a7d2bef622e27bd7b04e0f7d5f2b356d28575f84339e97c899507d2ea0d2fe395749df4be2f3d556d0fcf2c0e2ca75f5e924bb710f448b232a9d023e45e606acce2b0055edd8f3d8d1e3479165df905a60cab10492bd7c4a5abbd302f658d593e9fc5410566b36b7891d58d56617e77d6d5957a8b48397581bbec51ff621b8da196a417198184b64e7ad50b4ac3441129a07645bf4948531a4596f67b71812717a36ec1ea58a34025cd296475f61e74f8bb660bbf40e90533bf76dd8c3f3c2c3ab0121fe75b874e7ba71e4a5627ddf328e461439db9eea4bf8055f46ed435e6e35f2f67becebc696ce5b0c27ec515579cf83b3c64f5c7a51d42bb2898d3e6df61467d25abe377c699368faec5b0957133915f1bc2bace9117095827f09287f1ca5b1e9c845bd9728e190bd139d48ab7ac03202cabdcf099156f1d71fc74029f9f309b831f320a5c279db477335c61f71c2f7b048f1db55cca420b404dce55ae9cffcacf8a3cfb941c5b65350f975a5fe4c6b60ab74a2f1c901feb01e04abcf838fe7f582df5e86213636ad410277d5f1e647cc1df1cdfc23be3f87857d4ce20b9056d39f09839f06bbff66bcf4117ce6b0254f9b2a390d376aae3d173b41fbd514f73d1aead06253ade6ac62210d309e7a47db0880600b5cab331d02145963484b5851d1c72a94517c1292c116d8c961bdc38fce695ced61d3f32d2366ac1a0a52e2b0036b4cbc65819c132f159935ed553ede56d7acb693eca59d8758417836d0792d5f4961fbd9fb5d5f4ec4d3f63737ab7d5dd397f2274bf38326ee5105fdc2b13b2ea4527656a58db181ef864c2bbc4a7f70e4536226e73196f7531bb3b937d251d2118bc7b1df4ee9c8bab4379d66f1fa7bde359c69ff89ba1e7896b9eb498173f70c5c1c1771e1e1d7eee7cbafc2f10879b1122361c39b61c1c418548238ed4d55a8ba5e97fe496b338e4d7cc17c94c19e718e5c15f5e67a7a021dc0a8ace42e7a2f54478042d27b783fdaa9d93c5cf9222782f82e08c9f985b5a5617396619bc0bde1d74d39dafc637791bfe703b02677d6dbb0bf0c1491f3f34badd932e172ffff091bb3ef2c588d8b3755a23df24727118f92d272c708d9c5e9c7000a5e794d220c6705a8a3fd2eabd60cb19ba1a5499600537f24d6d609753335ffe6de83fbb91a3a444540fda46dc0bdf5c24f73a6c336daad51971f991a7e1b5a91af1da13691b1aadea8c2a179ceea05b8fc5d3c5c4357bee65b77bea22c99cb578f69f8981c989559337e0388eee6e959f8ecd1a54346a9ae2b59a7ec2baba55e8a4b3977db49a3ee1bb463b270f3cb0373877f7dd77df62c27795f91f565cbb51f7dc9d76e9e9acbb2de6e3577d7c6d3b8c7983b36e82a0b3bed8dbbb70bc58dc0e4fff4b38799bec0c0bb54530c8b10dc7e14be4831420703c976c78ce0a57c7814658c51c20cfe4950220ac648738abdc719c03d3f916c5b379c6e12b7971ca81f9e718c4f117995ce3be726cd8ee029e6f6cc9aabcb48c33843af5b6a39a51e5ca36e3844b2b8d14d7bc7eaef2541b1ad691ed31f666c6373aa6d33bbc255c1fe91a5c7420fae9f1d413a70dc33806b071e8175f1ddc27ed9ede9eb71c782a39806ebf48d529e5e5ac5dc706a83731032a8899cddec74cab5240f3d1b351511d6b6d41e8618be3674b2410e7048306081c6d7af412887625462725515700fe377b1eb4b4ab6cbe54c4045434b56542d8aa67884f9a665387fa8d426bcbeb2d36556fabbad9bac9cfa1ef77a5700e31ddabe4466ce903e404caccd92172e413a4ed4e5af6a677f4b1ee773bdef31eebd96e9d1d16bc2652c6f16b6c841365edb362cc5b1098afdebe5e7865c8d6e195aaed2eb5271d2733fbd13b9eabe463875aa7da37b36cfec437cf9ff13587bdadb4bb6d86fc63b7cb749ae21afadb5e64f0c5072efec2830f5e7ca542cb88f2a53f8cbf2b8ce930fd66a0154ac5228c0a3f30f44e600a1879d91719ca253c8ab91ca324f90eaa222aad240b33f1e92177dcdf0ae09b4ea6bb1fce675bcb4567056775ddb1e9a4eba09793be72debb93ae83bee9a4bb8a6ea55edc749e9a26bee6a40bdb3ae872e199137025bed28b4d2de27091897037b52ee18c6c2ab51abaea3be2895c2be3cab4c0401464629556c6930bed3c100a58872bc172fefc03867e7d29b92ff7e255bd89736eb34299fcaa30f452aa370488fe96558ad3c16943e8924f668c7fcf342dddb481898773d3c0d0f1601b6cab79deaa55cfb31a43dcfafced2c67efa05cb6f48d9f575fd7c77c03027d9c66efb27ae86f77f7247ac8d688ec4ba70171d26d89c1d5749df4e73def79b260f26256d23d8f8dba8e5d77dac70efb951fbb72f1f18f7f3ce57416fbef8a2b0eaddb7a06e3818d93eedd5cd4bc9271b8b5f8c2b1cfbaf042b6e507626d3f924300dd1a93ffa432a9924cc8440a19c7967f26f7b8db89cf9821e3f8c2531d6b2d6772616a62ae64fb0a450a667f395e7b9c70462dcef4ae67e992215d3cf6ca27deeb9112f4c489c36f595a5617573a0dc485e9d01bb783b687d6d85e7315a89ac88101b14e7e77e2a49fd46307d9c129b22beae16d12e69f1d02e781fc530e7c1fc7b38b6da15b0e7cfa39c02b485f18c9d7fea9d828815ae4024892b18bc24d172c93887ad39cedd864502ab75fe8a38958a7568233f820c6618eddb5807556a5e6c798d98852cbe872ea04649ef34b68a0cf2ab0edcc1c201d2837a54e9d7dbe133f2bf2d8bae048470ccae417ffd50f488ed5f9d26f7ae11afdc40d6f7d6ffa59af7951f73757d39fa80b74eddb99011b589b32c36450c2474d9ac16bafe53f5b2ef76b936401390e8be303a447ca49af5bc27e87a8c33deb54eb48b6f8e09cb7f442a7b2ffbae3de7181af1cf6b6d2deb7c8749a1dd7f366b8eaaa839760c36fc631f93cf3d2332530090421520494fe3bd4fe67e84d53d0abb96471ec02ac40d4836dc49a1628403ee4266960b73213fe57d3c5e24775ba6bdd66b57ae34b995d2d5f770a9cb7fa45ceca49a715f9385477d011c20b4c501988bed5c58b15f9d42f52e465dfee325e49b787dbf08ce2c097b54b4c4435928d4d8be96b06306acda124d8731948d2f957d88168ec145cfe859320aada89cd51e3a96c9b8af114a50c3a151da026df091a656c8386d63f0b82afd1a7491a4f752979d29794f48cb77ad5a5ace8bb346ee06489f4a88104d6360d62c2d236a842aa07b79dd983e862967f404c3d9cf5d2a7b3db70e06faa9de8e5b075b72d5b219aaaaa8fe81a8e310b14fce3a337ab9f270c9b316c7b52a8fc81071ec896973527fdc5a79df4deceb1b33e76e2afbcf2ca5a5dc761a7fed8bfd96c31bfa2be2e3c722a1fd48e1ce1bcdf0b7f5f0df36e96cdd57139e7654e6e506728e838bc0b0248c51399abed9297351e8d450caa7fc22b8f941167147ee5f8ca1378af936e561efc9434ce771081bbda6d4bf84cad8eb5711d682fa474f8d38c9ed639cfeab824c4e44c991d6044520785484bc3de51a775fb236d3b6aa5ddf2c4f9b3cec20db50f703b9252bb88e222173cde31a94514c01b21ab6ac0c6fbd34579b226eb8deab7c92d071e330778c6ee253a373ab3dac638afe87e542ee98295dd540fb3f4487de5c3ccb409d60eae65ca1117829570459c7fed8c96b6fe340ba6cc2afb9bf2a45d4917d3cc9468065dba516ab3689bd64a9b1d63e53908a6f927dde95847704421c3f9a9914f196ba3cc4f60d7f05fd5f732dbd4312c7248621cf84a50f6a6632bf2a617f3fa7730dccad8713757d33bfcf19efbfc738a0e03795f7821178a459cc349b972b83b5bbc25363d25b3bd9e49212b4a4e0e6b74379d74bf4f8481cc4486d3b8b6927e9633396e9c0e26ef3a9f811730ef1edee9ceba008c2656bbc2182e644c9be4396e19fff78bf9e45be8d541844d814917edb33fc406e3ae90442604018fd3e22d1e3d8cc840e19aa710442e00198f2ba15ce45ecce436e8bf89dbf9ef9d726f7af5aef9f6bd14c404c61cf340a9150d015e2149eb5b605cb983cf91b0eea4779e5a3077157868d4b87c18f77deca49bbf5d49970bcface0ab35d1dbaf5016635235c824baa422d691d60e203bd2aafcc73822f371ab23bbc294fb9869255c40d2d18d50c22dd22080240927018d27c5f8a851839921bd18e2aaaf3486b8249349be0949412b7735cd336d3eb7a174c1ac4a606f6f4873c86abdfd04c1feae2aa35c1a64316a0557474e3cc91a52cd72f620a5ff018ab3be088145e36edb60c84b27c77aba9fb75eb908c1333143c6ce830f66351d5b83bad602c5b092ce1790adf77deffbe7fb2f6e0ebbe971e82b32dd69ef691d771d7656d727cf79ce7328f23c06875d3a78bee8fb09767032bfafb65753ed11bc383a5eb0fd68b2f82863f31af99e6d45f2845737ca0622e1a73ccfa242313d173e7517421cf1e389875feef53694afac0c68ff900d9de2c28893ec44ef449dbf9e0f29dd742fc0e067c98079c0487ab4582be3e8e85873f69fb18df3cdd9614e5ded9c37bfd82fffc5874e9591b6032fe50a55d636009e4efeb081ebd42ec8dca55e9733d855f6a73bdb513f1f1c27e275d830da556cf338bec8daccdba6b71c786a38b0f8f32e92c4bfa1012eb4c4a9b531280796c043421c6463a4d51395c9f9c2af40f76d2acd80a488b6a28ad6599dadd56deb29424d3d4330a05427f19a1c82a78e5351ddb993466632ab4f1b42ddc2490b23b474b6c14893b6681b6a3ea87ccaddc982d5cfad9cf4eeac0faea394cede9b6e5de8bc1f221dab7d5f4d7fb21e20eff6ca86ad859dc5e21302ec66ad8391a0913290ff5b97cbddbb5605ea53aa953eca7e699df3fed38137af1cca8777d27d9f7777262db7e9489a270e13d24207b4ffc4d529ed3fd3868d7c25e1cb19b9dfc0d8fe1d06ed4083eff8460a9d198c22491a7107b83349e1ca8a9e578c52b19065f949c13faf3bfba417240b4da677b35de67564fc55e8bd1700f753db5ef49a4b03d2494f6474288700a118ed537f38279dfe1ff72f67c98b4b39e9e5cc8d2adb469f311c6055ef2574069312c9463e91508d5644379e98c25cd924b3df1b39ce2bb79564104baa917e65bce4b8f8a3958e01d5a62bdfa0588ffa614cb2ea0fbfa4a3533d5f72c03bcd9469ea64b990a314e732cb22848aada22634cc0a88a719e99069cbd6d93b5bd9d640b2ab69a71012a943bae25190439c3e6833a97cff62318b6d5bad36b8c23ad6cdf2e0d449ae86b077fd4e174b106c7b011407dc6d280f72414d7a08d9974eaadbb4fa5641adaa0f486744c6cede38de57d72d327b5ef6c4bb2f5e1b78c25ca23d196c0a2b3fb46bf6463a7c23fc38cc3ef55c35c962eff839ac702c3c21e13fbc6e1611528d4fd8bfc0e09bfc93d7f5c76ab62be84901678276c5db73ad8e532af1ca73db8a1bd677dc0a2375d2ae869b8c439ed578ca5b49e0d2b30de28164f9344029ac5ab30d07b8d70fa9dfb6583c18a984b8e4c04f066e486846263f683b0cf510a9e3dcbcf540995f3863933347e7759bf1d45b663b8d57d504f58bab75ac6d6acb81a78603ef7ad7bbd8143cfd427e9ad621e874c7bfd17e620cd4291144d10e448b825fcff72c5934e90656bb1d1d8c0d6eda669cffacda13892d46f96a21a6e686957d9670e1abd6956a654c6436a879c394ad2a739f56dad25e9c8836acd143cfd3839c454a2ffe174e6b7679f3758c416c87ec4dc7c5f5e23c6f7a01fee95c4db719abb9a835aa9f701aff9f36abc7e0d580861d4eea3fdaf172ae05f54475cae73b93ddfda37d3e5f5f139819ae34f14dd161253dc8ed30762675c005f789ace38dd3ddc1d461f7e70a3b2b48aeb28f2f727ad19ca963c14742aec2007f3fa3faaa1a58069b48debbcc394ffe3b61a5842e37b10849f5bb440280331a444ae088ca1c0501e170e5bcae409d38260fb256f593bbd3e54ff3f2c961c20cf94c00fddde74e1deb81dda77c43c5fcd5ed98eea0bb30e7458fd3c6264f5d495f8e1e9eed3ce97cb596cecb7e5eaf799b7a267080d5e8ff28777cd299924da5b81bdc6e9495bce836996cc54286b5924d9e630ccb58c6592a6528b9976ed4c08351ce18f8ac8e5a91714eae8a27924419d79a02289189c1eaba66a585c1b7746cadce94e4a067d4777c97ee7914b52e2eda8e9e54057668da8fba084fe5c11721e5421c4cd3d15bc92e6fe6f0ab38db24b0e43e51585f331add20ed8eb9fbc1e7e89f77b6cae8ab931767f5babe10680757d379bde2fcbeebd8977e45ed4beff98f5407c7cede59abeb3e68fac003fbb313aaa71dacb32ce66c794b35d83dcf789c0ffaba5cbf2df113389b77c3e337c18f0387bb38e4f8832953000af3e7513e0f5b4a349a5995e64cbc1c5e0b9ad631663c757ec5313f3f1ff0349b03ff3bf1a42d913575991a3c217920156465ce618ac34d5c029eb4b08638fae258b3b493497d059152c12c2a827841b64765a7eda04db2e3d0bb2d172ac13c7df08b8442572fe472393d616dd2ef40cfe38baa317c1bdf72e0a9e2c0f1f1c5976ae7fb4ab93ae676406d28d634ba5e8b1e68bd3a1725f12822fa9293ba553a273c2ac4593b2d3ce5634ca4491097f4302705d838601c9a0914ae39aa9723a3aa2d3a41a480f6c98aac13bac6dcd619086d6880a29b06994f72ba7c379d785755d68faaefda6a3afda8ad6f58d013afce8fbd3897f8a0f2bd2cdf366b7bd39facd5746b2aebb5aa738861f4eeee1bfe73db0286d84eb87dfbe1c9c9ffd51131c647ec647137cbe024f31d3e9cf54d277db5926e591ce76c79b99493de9df1b3ce4e6ee3096ebcc2ae43bef9737b0c83f337f7f777ff80f6bfaa8fb523975ed931858a092afb5be9a920052bc6bf1241916325532540c9023b30972323e8937b1088b7ce17cb5740e327c6abe12024c0b1b5575676b8679d74df3b3f2ee7aa1d653221d49d095c789e1190ff96919ff9b5fde8c48795f4eea477be759e5a6e1b9ea91c98fe258d9941436aac3458b9c5da22ebfd16a5a25fc611794f998e0f9c3ca55a236eb1001addc2250fb07071cb26529e4271d2ad9bba34e56903865f07dde9a0da93d2d62015e8acda2c287bdf6d6b2b234a30c1d350e7cf76859a65d5d91c39505791532d5bfdd61cf432f022274c6fa7c0ff5c71d72ff8dc581c765e29e89d2ef62e0bf5627936ef0f6cee939ef3062a1722ea15b5e79b257735dd6743ba932edde73dd81e1ec57e3d1e1d1c3b7fe3b8abebd4b970eb8d3f9d75ebb51d9c8efb0350b4ed88bc5fc2d6fd7518740fb3522ed260738223134e76003379f1b45c60b91b997012c7cbcd64ee99a5f0bc458595f03c048a335ef6d33c4a651fba309d767028239efb66ea0154a3d2684ebdf5f87029785e24784190d76a72b64ec08d0ee9c074fbdbf827dfb879d44fcfd216f3693de802c87382b60d933be1c9fa76a770835be0bb35691fb1f8c42ec940ebee84773aebe373a4e77dc216a15f58f5730a6d0f5b0e3c0d388033fdd2d8c3aeef9c63df5183d8543d77f334e2e5cfc48eeae568e3f583bb41d5f18ea32f3af05a9829986831af46a0a5554951f142df3309e3da184e158c515bcbd3e90fc442c22ca0ee62dca3cd2156259dc7a2f3a2585fca7ab614afdb9d2fff61617aacebebcdd5f4eea407cfad6e6d35ddaf906a430fe6f966c2f01552f1c677d1b4c929fb041e1c8933c3f39fff7cbe4cca7bd2bd7caa5e36bce58f9d55e0a019b0cdbc5a49efef59af5c1d4a270f9dc98e3f762685e9543a995deadc71361df6becaee19fa7b30fdaff27da43fa0136f620cd92f4b49062f1378848ce10bace019ce36b84a47bfba032b83aeb3b232f8e65ba2e32d6ea7c88db3f9e2e5c8f73f01ef92afaeb4fd9ba11e4ec321df78ef3c783c303ade3e840bdf1c7469747e8e79ea4a3af06c1112a7f3a9f354d8363c3339f0fef7bfdf5b9b5f524652e924685d7578156a7efab6b928d5b08122543dcf4a48b00ad76d240ab877976288c5d3e4f12b77db3c0db8144baf6adb0bb93ac7144a3ba827bae3a135c946a85faeb81655336c61a3e3392e6148274fbc5afab15de574c71893ac546f438887823a6aae7a5bef6c271998b84416930779bbd377016d8e5a19709d75de473edc095bf66d2c6d37442d46ecd3cfdc2e1c74b23be963fb76dd75d7d1a0951e76bb26ecd186b1f3b7b9baaeb3fe199ff119e94077d6b18173ece1099fd01bfaa2b30e3ffe1557fb5fc710dded882a1fe03a289189baa35872e0fa5ba4040738d138cd4429265fcdcf9fce3bb1b8cc71d871b23deb945b06dc5d9c6357c303235daf5e2c9cbc923155985676aa9c0e752e08ac4b983439f737cc148c82e65b97e261dbc0e384439eac56aeba923b01693ffd584c3e5c6ff7d9f5626ce09363b3cf5288336f5f51a7bfc3bc657e0f9ff5592de6fb365b783226ed4e7b7bde72e0b17160f9259901e2f8949eb87812bdd76276431f3fc81aca1e683bd5f5cc17ea17396a572c04895ab8690a652911f39f4c918159be70b4eeabf9a6e8d47333500c0ae50c9983c8970cc9cc23fdceaa348286f5876ecd17003400d8b332ef9449a1c91b789dd7a7dabb5743fa610e791e65786f3ac8f8e9a7c2a7e3e171ccd74385c5dde6d655094c60429b4e77dfda5668d60a1e727b40a77c0cac74ad4074c7b23b951daf3b94a6c7cee438dde17d72eb6771bae3d97184b9158657a0fdf58383fd3fc040df04e8864cc819fdc88ec30bd8812de1c96007a42878f567e018c950142911474768fb53e88237bd85f2afe1cd0b5f77325dfc0233f69aa10fca431ce2a07b1762e3bdf34c184eb8c3436a2889b7ad07874092637e76c7a0f3d48b15713a6f3aaf846dc3339703386b2fc532001940e6bfb1a541a3a8d3c49f8618e355924cd43c0ca97f060da87b9671739acc03d311c601774b9882aedd8de533d18c678a6b2055150f9e39a02a31ea1a8ddc9a2c1231cc0d2d785ae41851f0caae531f526b9babad8ab07152a9c436d80cd310e530564c1200004000494441546c3fad4fbc84c08d49c163f5d26d339a80fefe0231aaadcb7fc46b05efea2b2c3ae886b595954028cb45b4af01513777b878d6097635b5db37b79e8dede335d7dc3fd749efba2819f5d073d74be38f35f415f57eee4ebcce3aedc8eabab4b50971d6b918e9abc03b3b87aeacff2e3dfd2b70e813f25436caff3c07207bd330e5c7bec32f6441a73813768b675fb92be1ca09bf5a59071766c78176391d4e571c1ccaeb4cf7fde478dbc4cb79d7192fdc72ccf3012357d82d03bdd4913154aebb6c13b35eff224fb64f275e5a5667d9c22d8098e42127d93b6f7c67e77de68d5fc5195c0fde32c9880d904b46b2b2164a85d2c7e39205b6195b0e7c1a39900bc7e9e4a5a912e58e4d27a12d8dee10cf2b6bd11083d677e5b837bb909c64a84214164b75d2961b293b52c6a3ec74adbc6b8fcba2c4e65b47caa708f593af1e03934261730e8ee58c78820a782e14d9eec2238f7fa739f3eb6178a3551f31be973367cb8bb6fd846d8d12b2deb58bf2b50748fd0a29766a0e5ff2a61777bdb4373c7d5a57d36d675932636786e5c7c20839c592c3723aff198c79569d7410c745e294738fc0b3b781fbade04dc7b297d1a9dc74287bde434d603dcf73773cfbc4c7e4b47bfedcb9af479c7e0fc37d13237883431b8745a3ce9869a285f933f4b8c36d37ebaab00987b84d3e144071151f0342790fcece5b99d05ec65bdafede6267f11b7cd99bcda13b35cb07eba10f3ae8ac9e9fb8e771ecdc77071dde459c6aab4b6d1dea0e81fcf7d79d82cecf4d9e767e755e3d748bb6b9cf080e2c4ebebc2455698da98dc5cd6a454ba3cdadab483ec25dbaa1ae901343db045fa957f8d11f573f2cd775a594a3e8a8379da218c101603c6d090d00344748bfd56abd31f0815a0bb958a55c34b86ae22a8e65f9cf0f90f865a0ab4e8fa1298278515acf0dd74c83e946a7a341eb662eb0ff652174d53d39e5a4a383ed558cbc016467316c43d349670f78b69f8506f7320c5d1f0f0f574e7ad7c57e2eccc777ec8e605f591f53dbdc0a73cdd1511652b42f3aeb0fb6e5211e94bd0dbebc12eedc53af98958dfc65acba832b1375b0e17d73827b3c4e3070b7ade880eb4ce7cba38c9379ccbe5ed8706e701c73c758c72079ee534f1e0e3965c5d5b9f6215377a6889709dc720ca2abe48e6591a67de09ae8170253bf4c4a5971aa8d22db3ecab63a736e6d900e5f28fd60bf409387fd4356ab6749cb53670d663ec9db37c5da0c6d49bdada86f57d337f9b34d3fd51ce00d519f8f329c8fcd5517d0f31c3100b1a383e30b1c23590fe6dbeac28b525142fb69084e12b5d012cba1be4a55c23963483c8b175c0f0d27e74a07cbf92748e007a0ce824dbb865aa53b6e278d0835f1d565a98b6f95d9be33b91307be6d6bc449c7cc7b41bee1a4536a159803d9f151e9fe00e983b53fbdb9f895f7e9584db726ba75e9c07ec08f64128711b9aa5ace7ed50968b344771eb34f3d9df32147c3a5f7a5771a7da5a93bde1dfe48ce7dc2e32ae7851fbce3836f3a3cbcf0478ccf8f23739fdb05222b6d0a217d701c9da9237c152da1706423082210c4a91347c483fca4ebcec72de0be0e72fff17c39ff0156a43ea1317f340ebace791c74a8b31f9de714562bf06d121d1c7426944baea2eba43b1edd29809c0f360c6fcce9fcd93ae972e6ff3f0193f99f94e9a2cfc875b35c4d8863bd62c414ec3291de08553b48f9afb09b52ef5120e53f569373f420a896d53c565839db96914c3029d0e88236d00d3db2382f3c106c81b1fc6a79bcade4430f525935f72c59db64db524b2f259c5fa09cd2769375e161b1849493d0828be3c97be9df9b6aabe29afdedd839efee2ee3d87620862dc8e8df9a2d24cdc5f3fa370b1e8f7debf53dd27377d63d7707be3bebd238b9f65a2f2a0667bdad0e853c36e87d8cd937cc978b077d26c93174d5390f6831e3e9f0c61f66e28b238e779b0f0e01cc3bd0c1d6f92dffd83de7e5b4eb68c7e166553c0e788793e196975df6a0c787977eca3336c1c9d41b9a6494239fbdebc4fda3a238ef3af6b694b291249c715a68cb692f952330a0102adf5c50abac7d10cfdf72f9fe30a21d5c85c2593fd95bbd44bd09c8c1e45cee710ed8b5ba76ee93f98895d0af6dafdaec6330606e235b0e3cc51ce08501ff812657db1985896ea81d443adc36a237b1b078b859b88911062fb6194ba1b259b6d9d3b2f796e347e8763d734c8c36597a9bea69d3d580c197a43a9997c8a093d69d36564349a7b590b6aced49156973e29607d49a98f6aaf4cc83963dc469ff07c7b3d5f3273e6d94af9a15997e5c5b4d7727373631abe9226435bdf6a6fb46a8e17994277b6f7a6fdc433aea3caef8b11a003b0cf3d8d3d80b5eeacc527a1c74f3bb036f1c677258fd35dd577e8d7787f2d138eb77dd75d7f90f7ef0837f8ddfbf62a5e4f7198cbfc510f2b974ff1cd85afb530032ae8ca89d8d7044b8480070722998632aae434e20d1e4e46ef2dfc6c2de6b007d29bfff16c1e5c321cb0b3b3bbc0c1d277dedd642953e759c31b967059db35b5ce3a06fbc7211f0f0f1a2b0114ecac34bf1b15f34755e8ef7f93f1a5e9e6aec1670d972e08e3beeb81609e6d55b6800821bd956b0134a33346b2b88fe6d043fb8319814f296e1cced2dde4be4a456142d22d199a250beb1e529433db97b157c7590208d10104700a1df2a35de607e20cc449a6a11830e232761b6cb623e5cca1158038a41ddde097072b0baacd6a75ecb368b209a45286e3f40bd9349e7bb594dc701eb2be956ea6afaeac259277dfcc59bdddd45de51ae936ec0001c5d79a5bb2374d20fe3cc5d779d7462f3c29ec762df8ac2a33f76677d5c5267fda31ffd68da76edd859bfff7eeee6adde61b2bb7bf85b94fb4698c35a3bcc6248f2f0a60ead132883502bdc60b9da8da31d30936256b5cdd471df350f67591cbcf05a611747479e33fc8fe34f619d6a896635bcd1c9ea7a2e001cbb72fab5d380c097b670e9e89013905161d9ebee784758b0dfd4e34582aeb865b3ffddba539aeef0e7c44fbb7c15f1a724a52868d379969fb03759eeaedeba25c4c0a0e70e0ac3ff90a67fbba25efcda1e9f3e1cc02c7ea9ad41eca31f5980d5eeaa07dac8d8533148b4538b0122a6c26940fd19cdb1165972758f1ebab893ad95e897ff500e960ba47dae012d363b14a0958b01804e376982ed4929dbd9631a6fdb59f342c075c0eeb7798132c116cff2d3e99bd9b9727b2dc660dd63054fd8aabea6d763273ddf4aa8a2b5e5c51b8ffd758cc27be84e7abf20efe79eff449ec733d429ba18e2bbf33a343b3d9bdc767070fe63acc860594f87ee4cb2c2bcdfe3a7b156b784cdebab4dc6fbaa6f9fd4846d061f92db3bb7f7d5d393e9575dbc70f13f63549f95cb34da57a3ebed794b6182158a169f3188b98ab31f0a4544874c8a595288b831fac4c17a2f4ec2af92f1ebac02bd8f87b27a9fd76f7a62a6b5d4e05c928fae9c8b34c3f8bb07dd0963bc937f9ebdaeee4fafc5b972d07d8de53e0eba7da9d02f747008725b5da80eba67f2863db09d7f9d9fe67798f16d78667300a3f695656c75a28cf1876ceb9464e90263180d50f08571e6a8ccebb1fb1f40dd75a22c8563183188c1d392ea2c17d1d0f4654779a65fdc50e8c75412fc2cc4b47a9a6d4dd93880557bd1b4bdfca519a157ed27b31ac6b28cbaeaa46283065c89f6eac8b43ff653242701db9e029e16ac1aef4cbf13adff94fd757dc5c04d512e4e564eba30f59b0b6c4f93ddc50283be7b023d7df438e9c28f8f574e3ab742d9cf789007b91fad7d93d61311bab33e9e386ebcf1c6fde6acafacca1557b063fd02ede7836b7b477b0f3c60ed27efc6b7fe26e4e8c771980f649c136539bb1c99086a0fba197d751d2759b8b295556fe2a61d136764f1f801809738cd89cb57cc2ad9e5bc571da1a12b6d2b532f708b8ae8d19334dad89a593925276967900a0a76dd99312266682a2ad08a5c407cd156d3b1e2acb665ac575b5e52d04384c43b12599d19c01571d3cb7d23587f666004da46b71c781a7060fa17b48afa4351215b1483aa9ea05acdae76f5d2cec7b607bddbd1ea46478d7d680e9430553e2e99ce3600f7bc9b5d4eb63a8cae66a211395a195dcccb0d32bf508849257834242f00e05c7407ab8eda6a15ea2221f3954bf2da0e2ab7a53c5ff56e5eaaf8b35e7cf7674f1e6ecb0b04560f90da6682abe9fdd6e978357dfc3ac6275bdf2fe960da409c6edea59e1192e1bf2e6c1c741eb97dea02f110ce72d2bb93392011d1b934dd9dc87e1ee318ffd0873ef422aef6be8a917e398ec797fd7fecbd7fb075d959d779eeaff7edee74489360a8f00718a62c2a8671e22855536851a226a5635922e5a0235aa5853582300ea546028384a80821e80843553b8ec0384cac4c1c233101924e488f2926e3541c0a61420792344d0aa309901fdd9db7fbbef79e7be6f3f93eeb3967dff3defbfee8bcbf92defbdeb3d7af673d6b9d75d6faaeef7ef6da6b6305bbd8b750f266ac64f0c7575d4dcadd49b4ccd8e1f88905e3748a58e2ec0724f99217440ef9591fc57af80811ff0f5b17fd345698dfe40aed821cfaf8781592ce366cdc2a68be9eb639c81790801ff972a722ebccdfb1988bf65a662839ebcfa7e4dc2ad7768b3e903608ba337fdd8d70325baf79b5ed9487a067a2209c0c4dd2db8aae4cb7e194a41b3f1fcf9e16a0bffda1f47f9f29613cd8d1053cfed319b3f63bcd411a12650117d8486630e816cd2635221d974c194f21d7149212c82330ba0faf7d3d475540a044974afca01f1076180aaab985694a64f05898f5e42fd9095a42f29266bd52383ebf8ba5e5bb980682e7e1248021926179d4ce3095aa8bf3a857e321d1af5a1caf1e13c0fb70706d93f45af252c49d2dfa42d29577a86a49efbc711fe0d5f1c79274d630733449ef31d9eea93cb730d024fd8c0924b0e524c3d6917b6e1fa99f8b0d6a7339580e243dcc56ecdfc46ff420a49a17c2d1769c42bc6950fff280a684d97808b7e979091110b96fa3fb6f9fb07f98ce8f56b2e435ce347f1b644b372ebfa5bfdf2e6bcced093ee829a1281caffe11924f1ad19cad978da8a295b6797edf924f7f51c85403165691c9e7c9ef6122621fca7d153bc1a44f98f7468eb6b099a7dbff46f2cfb2730bdcca16f8999ff9992f41ff173b106a64782eecb75c7137f120448d2b074c8d455d539d2fc46ec9bbbcb8097f76fd224da3a8774733e7981745722eb9d68940124c573f7e758df929efb0310d7076071a31be0c2cc89528b2b964c8d8cf1c61ed54c9004e4dabba25bf5a7d1cdcf87b8bfd1557d85c7e634a0f8182a9ef459a7c75acade920e0f1216bd3e15feb074865e9cbc9768c9d693ad63bee56ba5785256e0ffc476f27a72d163b3f3bad88c411ebc255f32b3f25e992cc26983d91b54eade5fbf7eeff2e26f1ff942b84df0582fe67fc962fa57cace6fe1640703a49fd58cef8f971fdc58ca796feb8f91513b24b24a2eaef8fb95a7d988ef5cb7c9f5f24f0ffedeeaf7e8537ac3e7a7972c1e17a49ef0a40bcd724dd3a42d24f7d577ecc23e40e7add0b73372f2852b0ad703c75541cdcec3954e93b0bddd6ad497f3d281a92ce5cb221e866d86e3be3bafd48bbc28a6efa4cd26d8567efc1a8f80382a7e4993ece70b0df3b826b1cebaec1955801b5c70929c66c1a6f922f043b6a90422c406ebaff8e455c739b564b134a97a02e31971009b40ed2a84d1497045166de02e9da71a074a97c68c1b5fe8c7df2a59051d35481aca9908c8c8835c18fcefafe7e4f8b621ef821c6e8cf91a36ca48cea0078987a6273022b18a31bebfafe89ebd2f7d1b13a35aa5d3a72efbd87c7071392de17cf92f33b3d1e258c5ad3f9e4c2423f5fd016c3985e64fdd2bd9796f73d759fc6807c7770fd80bb886fbbe7c2856fa55dff21a4fc62de382a31a73f3539d7120e6665d275eff33cac99303ff70e5b5b82a5fe66b1984b94e93421f3eaf0f7378e74fb90247be5c39e8443e29131deb0520a640723338e78a3ed208ae6455d04c99d744f45168c24d68e6421f623fb48f41be9b1ff817ec955853dc7d4b209e2cb83a4ea3fe3f80f67c4cd51730bdc4d2dc0b862ff7470903114e30a63c1b11578663c381a62fc70b018609ce4ae2cfe3276f40245bf15e380ccb18c3b8491f522d96c0e51e34f18efc638cc83fd438f5c4e00ca58253db91c9366ce40b5560970f66f849356328e4287b0f352a1598509252b0680ef5f1c2f3f5917dee30152101cfcae3b66112ce82faf161c96398e03582f438ceb5e6ae3c2a480ed4b1f206d6b7a1b426ef585f929f2d9956c975a7fcc077598dc6894bd873b7eea4a26b7adea9d6edac4bf9ee0788df67de4f91280fa8b7ef5573ff8127e8a3fbcb3da7be96279f200fefbfc65eb47ab0ee54fe90fe3a46f82404e8de21770b95abb84cb1ac3934ff1f33f49da27e970de89fc0413ff2ff256d05fdedfd9f977874f3f4d5ab6d8c158b3b378fa692f34966eb9134b926992745deaee9216bd5abf4f59d313c989fa17591f7e497afa540b0cb7ade7d0f131593035f42274e2d073ca3ad7ed365bd1b71a720e5eb5051e7becb11701742fd90839461c3b03f41c3b1949485454c69963a130d0484660067c01a823cd2ca6b73f7915d503e29656cb5248f24d2420bd8b5b7aad816578563e764faa265c9b6d90b152870c725add83cf02b5655725b4c0381120c0587512288db933a0322b342c2f9554b953e2eec91bc8ffe688f56940b363b999bb24bd9375c184e323ea72e18a252f45d2170bcce903f319bbf9526d41bf1bc8ba247d4ad6c7774b3d9d70d81c78ef122f51665bdb2449d85d0ac33d41daeae422cb565ecb7a17dd90f2ddfdb16edc06868dd76e2f6555f7f7c8f218dcb28813ef3a71fb83586d0986ed9789238c6b42f2996e7ff4d3049b4e949cc98c1e5c7f55e55d0fab754eddc699d77e1851fb8c3d4712a0af048c223dbd8c58fad7cec9fbbdc62b737a13f483691f98fa15ec63fd20e9fd4f3079b30d7f4fd83d81b7e0ecce2d70a75b005cfbfd0e828c075d10c091c5e01963668c13478b6326c615c7223527caf15877311d33ca66d090d8f930f980cfae176f5c762c0a34ea10ff73816c99e3a018b45046b4959b79c404ea98fae56a00098ad34cebe159fc0f4ea86354a5eee22edeb4b35cfeacbbbb08cbf5f0e8318680535b31aa466c6f8e9a31ce3cb07e80d474b8a1e9e76ec7e878bf1d63fdaa441da2faeb79486bb57ae4a94b9ffe2d003cf24c46acadbfb827913ccfaabe4d36fdd26d0dc65af3389f5f6412fb7f897e0b9fef37dd430b14130633c6ea79cbe5c917305bdcb3dcdd793ebfc973f9950fb1f27fe2f860e5d6884f1e1d1d5ba7275ffc9f7ce9d3e62b0d75762f75ea18bcf6cda48db4d65b89ae37e13549effc98d2f7f8518ba54756b2dea9a7dd9ae04fc7756843d02b339b567255a0753e079df9c608ba24a0ef443411b0aca9d56e1adff598dd67470b00c47f44082c602d800dd40162b16ca7198811040381a25bf905c4024cc31c013ee20c0669559294b8c151238678cc32c5844a2303b6005abdea208cac501c3189364a0ac0f1e3e9e56b2ab0de166e5d6b1e69502efb7f3439ba2944bdca50c4206ca35e636218b76adfc328fe47c930be463b79a991ecafcc2f89ee712d49674dda31b7d832687bc94b5bd21506cbb2e4653a3e8def7179378cc96db23eb5ac3741075b7c078560c543a69275ef11eebe91d9d1e52faf950c87746b9a767715da6c4dcc21df3eda19d28c70ad3fafe530fec66555f7b72ca2ed55582f79b113d43a75fb265aec2061e3661c7dc64e819c7548b7c38d91c69fcdb838ca48d26b3a4f9f20debea458faf7e828f6caa8dae1dd20eea17f4c47ca9b6837d3c196f5cd226893331e24d59c7e7f9273ba1d13f7a6b4d937b7c075b6c06af10782abf47dc79f7e46741168a264ee0ebd90e9f2d4d8920513ce5d5ac79d986c9103bf43921d820c218933d135c7a8c3f1bec66113c06f32c702af2127e3da42c103e4f111a3f6cc1e956e5c89e44240190f8b3443adfa2064c4c9e2516afae0d284267d78f661aea9a779ea582f7921e873479b252f0758d3a1e770c390f4cea07b27ace9967b55a2fee217bff8938f7ee8431fa5f199e4ea906c4b7c3bacdba47c1aa75f22af4bfada9aee64665c4f5e3d9919e731e2b530fb14feaf1977b5c3fc7e5a664ad08debba364137ee3c92ded6747e21675f45737847f8f28275e85bcb5f3abd5da6f2635f8cd16f11dd2c6f59f27019fd689f37dad1129f0941b7acf3daaeeb31bbcfce1680e8be42a213020250094c4d56ca622898097305d04136a242dc93a980b265365280abd64bb30a80015abc016da402b0c40bdc4a205b3a74cc64b926545e633c325524603eca0e93b7de154e1ee347fea6e8c278d43a23988a1358479751a316f8f145ffea0348bd7ab5bacca03e0d7921e4ecd7ee0a462d2f03b063652992ce98bec8b065f43749b3ee753cb026e986b72fa27b9c0ee13bee6c93f551a1e031fe5db0d397224dc8ba0f991e1c5cbc78ef8f637aba4867fadb7b2e6981446b5d7747177f81fdb8906d2de5a4f989f55c3f02c6fb6b87e813e7e1521a27717fab5aa7ee0f0549b7ebb08cc61fd88b00f54bb3d5e311d7dfd514b25896394d0e49b003a043cdd161208945d6bd98b35318650c86a85f51e73133393d83dfdd9f7e43d609786432b87c79873e5011573bdf2e2bdbd5ea30a7cd2d306d8187dffef62fc71cf292eaf845a63330180f65e5eebb9c86191fa0426129f84b38ab0acd2ca0322e83c50888db4639a21c7f22b1f81d334d2c2826f2899c63151906a1a2355e4db73e59d95ee9927955f9197993817c29477539ca23b13785a26559dfc54e5e4ff90a0e0774aebd71b7f64cdf903bc4c4fcd2c7e877c94bd0bf6372717ee6768cb7739c176a6eea74858fafff21dae05d2634c91efe90ef26e3db193bbe49ba04dffc3d9929bf4dd2b7756c879b906fbb2d773348bad6f4437fefc911e2cda5831672d799fb46c2f8e3e6e521fed04fbb83cb9aa4b358c6e52d7446f641df5fd21e4f49d0a7249df658bfb4c8f6b2adfcd8567df761da664efc3df94fdbaee326559ebdcfb216f0190ffa9a3b21f1cd81ad60180008604990c44c0ff1ab50121721e5045aa1b50eddc029f1c2b1967060220c2759a2d37c00dcc8226003f494a17cde006a4101ec1117c9aa4f3038e06cb996ac3281bef48daaaef59b9e38015e51be8cb912c7c9ef987823a28a1ce8b3caf83e8ec5e5db48c98bda528d713ad93d39ca234683bb0bd8ecd2b486694ce8907497bc5c3845d2cb9a7e2f86f4b2a4ab6e7b5dfab49cbbc93f25ebfa47ddfcb5325949d67dc074c4bb1565b5c7eeee3fc19afe5a39b7c4bb96bbec40d2f7b1b2bbfc0517d22cb9dee72311df73590ccb64b4aef32a38e2f0d349626d374d222eb9d73acfaf1922efae31146e5e09bc645ef2be21f98495f0df048e4cfac4fa6b3bf9bbc980fd20322a933cf00d37fda6c87dc8c46ae751bf78effc5324ddafbcee062ab8e6b1fd70592f81b966c659606e81dbd002ac10f893315c5096c323cb1245c73c11ea60e1704c394380eb358e8c1473131b7f8c3aa285c21ea631e6d4607492c6d87360666e711c47397384f31359c5e64c2919bce279e1752d8f29edfa4b73aa4451a3cc38d4bdab60e9993b76fe1198fe1885d52e2f0c6cc7f6164937f9335af2d2045d3db76b9c17d259e2390793e78700d887a7c94d223b4e92b9fde934dd966f92dec4b2dda9ec59fe26e6ca374165bfe8f5568912f42949d78a7e2396f4536532316ff1f4244bd025e14edee361d0e116795748629e3f493a7f1edbe45cb9f308ba69dd56127427ffed36ebb650763ee616e816b8e79e7bfe3844e4be1068892c205620071d296c0c888acb612d82341285a4c60d211592bf21522c2c98463e00de29c80b901e206642a4476b15be06d9a4225040ab84204d45a8986449bdf98b1e4b33aef45abad69d7ac8c96a1206b542c4496097266b6084a738913113436d79b2f3d77757bb1f335087d03dde3c5adea0396bdd0649d7647aa46525afb591a47b8cccc61f1df2f0a824bde31cabfa1bcf1aa73afd6e739ba0e3e621d30e534f5bc4874c43d69ff7bce749aec5bcb0d6d572f58310ef1f94444bbaf725d5106dadeb7bbcb8a8f64ff721524838c45d429f874ce90fd9e521a49bbc1279fd3e80aa5f92bd3b2cf578eb81537e79087c88bc849e9f59233c5e0592cfc94b42913e42205b4326481f65e22657fa4dd2d3eb461f2147fa887d6967f5883b425ceb605fddf5ef7d2d5997be5c4b664e9f5be076b60088fab5457c19200c83ecc042ff8f0587b06345f014c8e4d221d2c671289671e6387608198dac434ff935a136adc65465d2d0a3ae42f7f80da7ac286adc17b79380e4280355eb21ab9fc3629d4b1455aee61d5d12563bef80a4fff361730992e907a7c0ae5317dde72f79190f909eb7e405756b623e25ebc6dfea43acbbea01a0bdedb7fff6dfee2abcf5adddcec004752e7899364defc9acf35e2fe16c626ebef6eb368195a09b46e3c66d823ee2d64b74ce5beea2dcf4d096eeca1769b6cf7b4ed3b6fd2de3444e47ce1b44e5e7abd5f9d6733ae4537d51336d2309ba1fdb699ba03701e8ef6f3d3a6ebb4e73f859da02abc57fe52d4a51cb732c134158ec8f4136a18d34718d93001b58c6937ce635a327905a503418121d3dc4209338e3a3a784e2876ce596a981442b3bfc648ab55ce01e713a21ee4377d54d413e89430932150f782b9f782f3cac349ab2d8116fcaabd3fa9bf940e9c9c9f7ecad78d905fc13628d401f6b384f04d6a64d227b8a9f9c549871ead05e5bd3cb927e78fc808bf238089f7997d0b4cfa6f1d984dd7a73acc93a9819fc93ac83b3477c20c8fbaf852dbf21249b171b1d48ca71f75912e3b68cfb5ad225eec4cbaaf7b17a6b710f390fc1ae7059dfb58df3a745dddf5a22405e32122ecbb9bf7f59da1150971f2422873ec56badbb71f439fa48f5071e7c8b5cf55905ed3e9ebc53947e45109947fcc2d5019cd07b525faf7149d2c52bad378977170832e568eb5abb1d3fbb730bdcc91678d7bbdef5650cad973970347838c20aeee9fd09646064dc243e57c38daa8e496b6fd831665e4e040b8f2b525c57956538dd985ef91c6be6318cae2499593fa3d078b1dc14fce3b4762dca3c4946ce700eddb57ff508a1f5738e427d8d67251dcf9bb13c797894b2b796bc283e3958f597252fb8cf3b3e39feaf3ba92fc46fe7384fdb7605ce72b108bf691adf847b62f93d45c8959580769e269f4dac7b02d39578b6dc79ee544effd492de799aa4775897b82b48fa34fd3c7f59c2eba14fb1398671c8b7a4dc53366bc10d311f0126f4f5b296e9d216cba02d4e2d6f197169b326e7dd466711f46ea329493fafee73fcb3b3055cf6027dfda301ba209b36698ea02ee45817548b131f274d26fe0b7602a0e9a58034e82ee98585e65590301f33f405418018a512fea451a8d241dfc89ac61f826555715ea8382b98a2099bd7ea04b02d079dd6a596de58e448372f82ebb5ec2ac962c4a193bc99867457270f02c4efb23a6b8e9e6d00ccb27ed25f42194c67746346d70293978f7187f0c411bf3c80ad2e16be09286f035a7c12927e9c971a9d26e9535cb3cccf86636249dfaeae6d72327d8b299611e3c262c1b857ed1fecbe17d20e778694bb8c45eb792ce9fa5dbe0269975463690f7157867e902533fcf024f18343c643b689871c140937ce24c3e615c6c9a0755d9f9d4e4f9de8239441dfb11be4ae4b967e49006a5dadfd35e2f615fb4fcee95d2a39a46ffdfa7a2deb6442273fdfb55706218830137622d2572a9c79ce07cc54d6474fe41d9eddb905ee640b80777f22e3c5cecf6018685955a2e73a1a182e85dba60fec8f80f1c8989e7c622b118e2a51ddb157f88f5805c8e088536749e14d9c6352bf628ae808036e175921a233a910442062ce370639295547917ffda47e94bbc4afe43b3ee57af435d6638ba95d5e36247d648e435d72c714fc3a679797c76b5dfa850b5fbabfbffbafb9ecffb526e6bab77b8c9f362d4dbf097ec9e18b5ffce27ca14e927037319660b6057b4ace5bd6f4f64fdd269d3db94dd3b6fd2ddb6e137ee5a6cb5d0c775df4dfe8d1049b1f3c170fb2f200f8f8064b5a8a98ecc0c8ab572fd7e5e3d9a548ce4d1996f3b550b7d1b45d883bb56e7fda26fd9d55308d5f2b9c3d730bd002f7f0b65e40f162300e40cb5a5c088b7f205f411e20e95a4211b7e013880b620603595e6258444666c49b3d566ff209de1eb18c0fc80cf0aa2d382bc1564080d62d35b1baa0c8323d02f15ac41166ac559ea4929e8c3a662e1df90ac4abd2ba186db24b5ec477bf83d535cec35daaf0ff24195e6f586e1e00c7d5aabefd5223659a7849dab9873688fb019674483a38ffe94f3f87b1cc9297c34338fad92f359a8e55757e361d12f6a9555d3ff53fb9f01f2e2c1e78d1032ee99359ef11c99a751eab5ff2c2d29d9d6fc04afe36969b7c51916d087388b7141d7108b83fa25b344ac0fd7d42d4fdadfde190359d5c4a93cec36489b31f21a3df4cfcb8e64b0f328bd2c49b84002e7e7e7f65ec24b9e033c93475a77318d48f3b9c3c47c136befcdaf9bdf992dc4be1e159913b73fb86a41373dd8713784fe8d79d69169c5be096b6c0ea4f150ef770607c89a9191e22abe38d80384afc862c4bc12bcf187019559b3165a591379f634b829fb13af2a8d232d4e205b5e239676466dce6e546c4659e895c49b5e5dfda258fba0ca03055b5fe6ccb0d8efcf5e5ea24fba567207b02aef6561a63d6b65a954c0d34116d92ce9dd10c7678ada3bff060f182c5c5e75cfaddcb93c5bfa2cca3dffaf8277f364a38dd89313ea0afab70da951c3a019d8e8d95d8df2fc794744ee33a7e4a449b6c9ea7b7f3b7db93dfd4edb49be9363957a784fd5a9fb3cae67bc6724ee7592f6d518ef858cf756d133fb6899fd9827e564bce7137da0290d63f6b1e41360353ac0b72565cd2044880ce242d1fc5bb01bd240a94a22021dcf299527e498d71a64bb42d47d4ac220298c91a3b470906bccd2fa84e34118f2e405b1595523a1361bd520fd5a3974a8e6fb49e3c02fc2e5db09e127ef4f85d228f46d27f1e9becebd4ee1192ae07f8655ce702daa0473d6b52ebaf873f200d1e2c31a45ffe344674ede853929e8c9c1cbbfa6f14d33affdde636599fd6ebd716bf7682b1e1c4f5f8b689698787177ca0fe88f6fe4dd6a17f23cb5e0e69d758d3f300a916f2f1006959cfb5908f074c21dfae392f93b913ae615dde2a8a19ddde98f4614597ab1769408e1f56b2e14f9ede5826747ff8f435974325defe933eb4ee78d54732329470842883aec5cec737df17923e0230768ef5b497df79871d5f36b2a77dd30749ef84b5ed746de6d0dc029b16c8db487776bec2de5e7d3e8e7ddf81646c0e9136409d719150825e143b061d8b1e3196380e0d08ec1cf5b231347a41cedf881e7e64915f67606cfa5851e6109758ba4431458be7e657a175ccf83464b53317a454c679d57575c815fbabd85af7b1084d4e3c6fc4087634afe1fe8a75e98a5386dbea5e7145ee92179ed37905c6ab8700abe783276fed8bef26e9b7dba27e55a2ee976952dd1392715ab5259afa3d9a80b65bb145e8a71670e39bf84ff5b5fcd49d92f3b3ea3095bd965fcb76cb50eff5afd77112f3f65fafab9efed0e9ae49ce9ba03739ef76f1bb755bf477b60e1d77bdf599e59e9d2dc052b00700b03f1c7014508131814db4cb1b41f107da0048f90be73c9cb91b8b3b693d8a25c67ed28c80a9fed6a35efc868b565b4a9491c7b44028fa29293241daa2444e06e8cab39d269a841cbe519235aa725db652ebce15aaba5689ea405c3d38bdcb406988285e5378f3f072f59d90caf5786fcf15249d87c2d9bd698d05eee4a43e09a940addfe3c2d185b525dd70af4bd7dfc7f5625acbdfad6e93f5e15efe9dbf73b178ce739e730559f74dac92752cedfff6607ff7dbdc9ed1074043b621dc2e7971f94a3d548adf3f2358cbaed55c8b7aacebce3e844db3e72a62472b83bbbf273d23bf3b9d28e43b97897849e33f3d877445cc5f9dd4f0e8536476affe84eda37c8af08f4c8be5af933587e4bc08fad4a05e64fd88974cb7dcf5b83da95f8fec2c33b7c0ad6c01b0f74f3b8872912b7e66dc64f804b71d3c8e0fc759b9e52f125f63c77853b32451e3894a2aa6fcb1942031261375a5a0124b61de31335a91da57dd00b309f159faa8fe9070f4385665f319d35e5a8ff19c28c87c98fece0fad963bf586692463076fb0379c2397de5392cef7ac75e9cc07a7c6745bd30f0e1e778bdaafa7a8d7034fbccfc70d0b163fa6ba2949bfdd17e4d724ea56f02ca2bc4dd695eb4312efa7c9a8f14d3c5b57cb9ee7b6dcb67b9efcb5e2b7c97a93ec6be56bb9a9cb8fbd26e6ad97f42b2ce74dce493bd37adedfcd3a4c49fab5ea34a7cf2db06e8193933f03d9b918323eac0d90cd80622f0bf14d9d5a46e4e4815e014fd046ae30b752a23364dae506c06afe3d99aecb47f01438f1e6534036ca2b51c99175503242a04cd49a41bfb5c06f5c65d45ffa25f51eba5966a3ab7c096632d875b2505cb914a2cce2534c467f838b86f1a867d4e454245d4e5ebc9ca52e219a1b894596f715495fc69a6e1a633b24bde59aa437ae397e4d6bb7e53e9bdd26ebf51d60ea1c4dd65ff8c2e3e5f39fff7c26b807166ce1e84e5fbc9979e78d10f21fc98e2f58cb6ba90bc47bac5f2fc6ce8f0e2177adba2c5c5a9e75ed58ce3bce8b3d6f8fcbd5470f886fdd45ac084726f144568add2639ec0ff40b7b8a64dc1e9b43823094e878a4ef10a09ffec78a71fd79f9d6576e17fc69dd77ffcce38af826e7b7dbd27666ede6c8b905460b3012589fcee0d07aede87030349e3b5606e9ce85ae6284b56b3b54cd9661e9d8f2c3a0aab134220c0f7c56a0709ab3d86ef91123de4c285297f34a8c3ba95f95cfb06749a2e96a502727169ed70bf0f02a6b8514243ffadec0335479c3740f4456bbb8e2c53b7b5e729bc3a3932b34c29274ca5a5bd39ba42bf4f4d3cff9efb1333c483ddcff8f32777efe15af78c5ffa5b7c9f9b66bdaad1ef769030bbadad124b2dd966db22e119d7e8cefc94cd99ec86ec4f2d4656dbb5d76bb96db7e8971fb890f71eeb0aea4ba3f1d8fdcda32beeddf26e4d3bcadbfddbe9b701639efb6b01db6dba2ebd1f11d9eddb905aeab0576165f1b601ba82a71d16a5900aa3d40d4c32a209002d609e237b6d601e301480d8b90c1f19c04cd481157568c446b3d19f1ba68450f71922405445a1159a928257dea2612108ebc424c257c2c4bd5668d88ae23dbbc44ac1f38b580fcd777d0dc01f01feeac967f0bc98ff039e790891db81e7d1bbcd7247d792049afbb6b9250c676647b5d3ae135ae395e1bcfce29f0b3365ab2ded6a326a092f5dffccd0b274e726d8d62494cc83a6df5b778e8f3ddeea9ee968cb1b0f32349c27996349675e3fddd7acdbabfb3736fd6ab9bc0e1c55afa00fedc81315ab9740abb81444341ad6c3824ba0fb4fdcbb5ae92805c249266bfb187e4d67df4239b4c248c4e46f16ba2ded674721e1ff2b071dd625d2f7f5123c7a5f5f30cde75e1eee872fa2069b759c9cee7b905ee6c0bbced6d6f7b1103e32b5d52227e66443858e8ff193b8cad5e3e186b79aacba841c4a5648e9f32988c21435ef588d6fee59f38f973746688d78c635916e59460c9753800994b9457b72ac47ecb234643b97e073f35a66c22e2126939b95258fc24b3d90f9350cc3c9e62e0f5f0a811627d91758c2fcdda4fbf7d746b5d3a46989702670f8320dfe6f7f701574ab4dcff458d1e3dbeafe6b65c32dcc4539aec5afa7a529aba9da749f9d4ed34ddce338dbb1ebff9a6f9cfd2d304d809b4754ec9ba714da43bbddd26dd57735bb6754cdd2931bf1e723ead7f5f7ca8bfbf679735bb730b5c6f0bfcf22ffff21700875f55ac865cb01c71aead1c213d85a2c54d1a1ccb66122092e078084b65b580f80c39913400af084019bf004678fd114bc7e82be0258d8203e5a4793bd47ae08b6b8a72b18caa84cc290fabbf327dfbd41c9a526a998b208d46f27925eef7b3ba1e5c441cf2fdbff764b1b7b90d5a4939432bdd14dd252b6b92cece2e01ef26ed5ad23d783aa5f8d9247f9374a31a6f7acc4ec7f424cbe784b7ad46d32f235937bc4dd6695b97c1fc2548f7a3ee0493874a59c7e24e2e98ce21d0648ad7df902958869e83c8f4157e4cbc8ae5347edecccbfcd6e92649a46cfbe11033c189bda841c9f55d98a84f1a7d8432bc50f5486ec2e9837b271f4d24a718d439c5859f1745b73bb0df0bff07be56fc1ac7596d768d2c73f2dc02b7ac05d8ade4eb7ce6470b795d15077d29cfb1c050c6e9f5e556a252956584056f89c1eb78338798eeb126f1a560a4a96cc89837929c8df6ae96c5c1c45deae21dd15a966998b189acf28ee59a8fd863057f8eb52ed316ec95be7a9d96730f9f3f6a7f59d28d6d5ebe982e79094937756a49370ca7c33c73f4b7c0ae9fa1022f1b3214163cb9b4b777f04f8df3e8f17dbdae7924f5ba9fe951ed790d2d4d2ccf72af96b527b2a97b35f9b3d2b6cb54c6b8a9ecf65eea9df699ec02d33a74b7c9bf71fcc099b47af236ae8f9ec83b7cd677e8b4d99d5be099b6c0873ef4a16fe456e583625a080a7057a48698364f6484039c028f0511364ab99144480b3cdd3969c27a5934140fd07292d8943c67158dfc59af98b422e2017641191109b864db09a0a1db5a448f02f1a9a8ea6ddee4ccc5c5d000b00be035630cb9407b14237ef23decd3fdd3e6dc3ee0de47bda4011a1eeb78009dcd5c7c8b307ec1bc48fae43995b6a6df7fff21b747eb2eddf6c3a33da6b7cbfc5c0bb7f5c8efd5930ecb5e323fd2767b60a3ab553cf68f56475fc62dec8758737e9fb7d0fb27f6670d4127c289b97f66ade9bd0d68cbd6ac9d50fa8cfd2e8ad20fd099bea20e644ceb7e5e31256c5fe762c0977b650dacfd9bfc31e50d3d0872ecfc392efa1ee3b39edd893c3ed93f39aa09c62ddc2eba5bd0e522eaf7b545fd786a51f761d2e9e41dd5f3696e813bdc02ef7ce743ff9a51f25535de380f234b883b0935b61c498e259f0dc1673c7fc1eb8aaef44c00c483cd59b668da80606d3d5e2c3bc6cb485443365f3f85ebf38281b232361dba2458d8706b1e29fda002498e75c3ba4aaddecd78fe2e76f1ea7d9ac2d205a2cd9b471dc6b1a683ebebe78fd6247d7bc9cbcec1ceefdd3bd9f9fb64fa1d79101688d0e845b181159c1ff9e4c73ff95788bdee638a0353ecbc6e05e708ae9bf19cf42ba27b826a5701fd2d3825a92dd36ecbdca8dbf9a7ee593ace23ec67c9de485c9372f39c45cc8dbfdaf7ee7a2b371f730bdcac16f8d0a38ffe6b06f05705594433c0b081af9614548c67815190f4702941fcb8eb34ad1cc60b9c0148644d374fc44c338a336525496593f444264bb444aeea968c912dd0253ff9d417ddaa1922891a09a9e330be5a855191f8aa023bafa3ce59ab58919bf372b7ace60747078b23f6dcc3b20e3127eef285daed85fdd22f603795a853ce539d734dd2593ac70e5d58e26b9726c77d8ff167db78ee09a789ba6dd564fdf33ffff3f730a8afc93a6df90ae6eb1f85265fcc446b47d13ce624ce2498ad15c95fbf7ea6eb7421fbc1dab646dfc82df1d197d2b5c963d79b74bcb5964cf249a78f2214828ec21d3de3a26f745ba43cd2c1745fceb280a7a8f369a2ceedf24c681720ea58d3e9381075f75077e94b6de1d6449ddd217201e8041dd5f3696e81bba0051e7ef8e12f589ef0b0f4c9eaa27341c8772e5cab7259eac2307024387e6a6c1966ec495807eeae2f8437809fc4682473cf258e3b428173b13acb19b3d472941dbc271db9a40f697370cfb5483c71a9d1286be434f2bde8fe0eb07a8dd3d93a5be9ecf0a2c87a084f493ae9a7f74b07ab2e73f7ef5e6cf6dfb15c2dfe1c7ab316bde7b5544e751cbbbb27bff7377ff353bf58a1eb3fdf0ab2de770eaeab163d41b56ba6a97f1aeef876afab8073849c205b4fbb2d6ab8fd4da2759bb4779a6eefff3e8dd33f25e29dd6ba3abcedf6a46d7cd761bb6e2dd3eeb68e393cb7c0336d81c71e7bec4527cb93af1ab60c6d86c1d470139050f00be0027a59eb27e9018504a49024c1304712d6d6167366ad39ec28e0ab5888790171f1f2b2b9475644c71a9f54c1188f18ef921581d688f892b11e548af5a584528645782bb4ebd6505e170ec2bf75c7557f7f8793931f64e71a487a162b90ba3962491f4fab3449378ecd497a4bc663d7d060396d6b7a323749070f968790f4cf9b90f4d6be3dc63bfe73d99d4e3c7e4f09bb0455b2ee3a6dc8ba4b8b42d699041f5ead96ff10b16f9324a7af550f483fb21b541c12e3f76c2b96d6ba58dc47df942fc412efaf5f1d80ae460f481f4e62f555352ac249be603e3d592ee59d22ca37bb1f8f442d5697d0bd99f82b697d9675b3015c96f9d8ff2ef1c71610f331b7c067450b70adf9a7e8ef79b746969138401807c59d455171dbb1148734d2192a62759e65028fc57f2583ff62bc0381a347b5791c732357c65796478afde6551faed9d49bc1e9bc525a8d64bc3bdfb8d40597bf5c345061738df27e9ef82b48ba8f896ed6a45babcc03b9681e0860ddf2fcd1d4927e70b0fb27f826df4e715f9cf2cc4a3d3247d224a91ddf1dcf7ba724fdb7fdb6dfe68c75ee31dda2756ad8d8c6ce73155c23e186887a134edded096b3bdce5769e0e3f53775a6697d5ae3af54f759f45b4cf8a9be639cf7fd677e8b2b75d759c257f9eee397e6e8167d20280d0d709a4926a6f294a7e5dfb379038e09774940b9605ca05ce82a0802a11265bad0808de7a8aa8675114197ce6e7088819a15fe026665c0d542693cc16fd41e984cd5d75511921acab05cc68701fdde837a3928808984e14a96e295524f859f9ffc7dddde37f7e9aa4974585252ef1b47d454bba243d8a615fcb050f938214fb2779d7b00f46c612da243d729cb6497a8fe9765beed9e29e35e19c41d65d1ae2ce0b3f88f5ebcbf805bf2613b9336efa60ba034de66f5afd67f45c120823b39e28d33fc7949e2e60dfa03fe6f67da4089b4e1ef5a75f108f589304e5ab0f392c94242627e517ffbe1e413b7b0abc7081874a21eaebe312be62ea830cac53b276d5504fd09b94d937b7c09d6901bafb5fcd5da960a89d9ec371c4e118c9d8c960c88214231c5d9c18918c27c97a2e741d5f195ea6251b311975c27891f12123409781a6e4c6602335ca2d727da8b39e1b51293306bad6f86f94b559ad1ea1f0bc75d4982c4877f45d310285f783c4823b0dfd09374927fe0feeedefa26be725299b1ca288c5fa3d330731658940d61b98f9d126e74f3ef9e42e1f24cf3f904d6213f69b8d0567a3d4f9f549ca36393d2f7c0d35379c7cde24d9e54f151a370d5fcb7f9e6ef3b5fef35c65ae96dff4f9985be066b6006bcabfbe88b6801a7809917569818028080d8435006111f820433017814a306aa094b44b9bdcc631a4be14202472912f19f00ab4e80818139732882bd836cd1881dd731f025f8a4b5a2d53a0b42ecbc9827fabd48246d5619aba80d39882a8d1ceeac1d5cef1ff5e247df3921ac36b423e7203d21b924e1c8c3c16962b49ba3b3fd5feba5ad3fbe1f0beb0776cf7d81faa9f95ce36597fe3fbde78e1fe8fdd7ff2dc279fbbfb344deb72102ceb21eb4c8cdfcaee2f5f463f7b49fa4526c1fea1fd8147fff197f7371ebf795fb4d94fecabf5bb57ffb6d16b4dfb10eebcf490d145d3c72ccf0bc8eaaf9693e27027723b8bdf307aebd850805cbe6d52ef93a44bd6b951decb5edcf1858b9508ddec897953f2ec9b5be0c65ae09def7ce71f61a4bcc45c8e97f5e0a0ff87843b208c8ee358aaf1a42124cc3b23c5b19291537303de184fc4f6a192a960addb3b5d35764bab65f608472a4514f6a3d779620cf89450c5abac4b467ef50892af647381cd5dafcde8c418d06f1e0d2fbf82a4a3ff58928efb5ff07925d579d91a6352a1f1ad1b38527a9f169fdadd3d7883047d88c67199df34bc58fc162fbeabfdd85b56c2de645dd99b850bf54b9c2efd86423d816dbb37a4e419086f97775ef87a559f977f3bfe7af5cd72730bdcaa1678f4d147bf04087e2c044800a520810f2eae6dc20031604cd282a691b13e801f9f02e1ecb0a24cc878e50d990f78019aa851933904deb278e202ca426a52950d296255327296668e4aa536a467094e4917f1d24f998d826a2b5d38236f0ae6b4017725767ee464efe4c77ccd7be099bb9dae41f7aee73649678dfa71767c51651d45d2f74f5bd20f2f1c1e5de06d9b8a9c47d247fed9192dd0930feb60ef7152d2f2e444d513d95806b34f9ffc12367d7988dffaf99595fe617fb1af12613fb40febb30b18eb4563fa5cc4ba9f99969e8d9bde98bed6263d53aaaf3a06d4475f2e95f1e71401fbb04551c66af5262e28ff81663afc6b2b5c5be4b0f21c93707cc464cfaebe4f49d297e315e353a2eef7f702c632ba5d3685cebeb9056e7f0b3cc4c19da797f798480d322e7a3c39c632c0aa7263ccd410490af1a6f38f8144fc7654d5d81de367e87340999653e61d428920af03d9fc0eba0c6a1d952a630447e24d1f61a208bd97b17d7ab94b84ebb4b5c3cb20e9a71f1e65def90ac6f53753cdafac8b062fdcd18cf6faba9aa60a858cada37c3c4bf3e0d3972eff0de33efff34fb6c8396b2273fcd670cb69c2eec5fb94a86f1b374e65ba8140cf953790e5b4685b92b7ddd352373f34b5726d93e969d8920d5fcb3d4b9f796ef7f7b2ccf9985be06a2dc0babe3f9df480dd94d80034013e2de3a0ab260fac924a8883e50e2abd26db6802b9922d1203bcc820394a6ed5120e8932bb7846d9c1df529cb2f49a26d957965a843105fab5e88b90e6d7aa6f1af2a94d321adff5349e44e43379501013c68f43a27e4c862e37ef43ff364977b9cbe6d9225f82517ba70f8ebe5eeea28ee73e519a9aa41b9a5ad20d377ee89f8fcd36655ffdd55f9d8b1f27262728d7ab4fdae798dffbd78e17bb7f81be73e8dd9bcc93b8b1a8d97fec03fec0760512dd5bdf2e54fd203ebcf6c291470b3bc2882b923475246c4af4a0834ed57111b49729af4c329abafbef39714ccc741591b3cc7bbdf20592ae31fd6ac74cd2afd63a73daed6a81b7bffded5fce32929717a23b26181f8cb1f4fb5109878023607a243d83c69c78325ef009d004eb62d8115487c33603ca60c6b211e61d09093aee4a57e91b59cc8cf9dee21cb3baf194f3eeab93742de95e57f7b535aa263bbc90f0bb7961d13fa14eafe762e52bf3fdf9c2b9834ba25fc8efef1c94da0494f011579f15ef7a58fc884687323cbcc00d057860be3f4779787e1376d7ab17ac09bd068b5e3263711e7d215fa16776fe8c89fa332bf6e6e4da26d1d3f094ac9f45c23bdd9a4cf34dc337a796b396b9056e6e0b700bf29bbd8519fe21c111f7ba08d026eb7e051ee2ca12d968281e1649ce6d4a33e65f49814a980d2483afc6a989b36524ac94e08b1cc9461534ab534a5fc95af32557f90389d5a5f53ee92e46042c43aa3c7194a53e9ee8b3087567bf5e0b5aadde00aeff4f6b7e2e461bc0ddedf5e7043d30801eb36bba0f0106c9dd8271b9dc3b2e92be7f8aa443ce8f0e31974e49fab3751bc66abdeb3f6f5b8aa6645d8bf3d074ccdaf0ff9b8bac1f4dfff1b7e4a8ad124bc2dee6fecad55b9c44f1d945467f8bdf38ba4afad1d0113dc61b1ecc3c7ebb149fd1dbe2cf0bbb28c10b48fbb30fb07161f01bc770f4cdae11559f3ea32b0c9e897e791e493fcb72d6f967776e813bd10240eab74ababdf0cd78492518108e0b870a4e791d09e58fbc389dd4c27725a5d28ebb9c70bdbb29649b224087049b2b423de6c8a30aa532dc2d87509866edfa64bcd3405222c389f981e32771be0b83cc66b98bb1e3d858d205ff7cb2c30b587fc078fd2f89fc67d4ecf594fc55994388b01eb60353123e2398a7f0f8f11067c49f344a2abcf313179e78ceaf7857b03f2599b34bc5fbb38e1e771021ebdb4b634ac48bf8b5f033f450c3f9985b606e81cf961678f4d10ffc71c0e65f4944c43e4153300a0efa251a70d740449c44482c72b41b8f2ba91168452c75a8c5a3fcb86bd9493a91829eb45c39f3ea8beee4de68a9423c9b8efe2063a76fd785cc43af96576be37fb22c166fa2ae2c51981c4dd20719ef142ceb21574dd2b3573a71fb4b96bbf04ffc7ae5b1249d074843d2cd3f5d97de17eed38bf92e63764fb7405b927b329a2e83e909ec70eff05e9628fd0cbf3e0f7271b66f3999eb768fa3fb554f23ca78c3b8e90be9b39c06a1af3c5e18161d77c957c882cf3138df472fe510b4c799bde2361e44ff324f5fbf6f8ba8bbaf7e2ef0e873b95b0039b8ccf748bfe9578df7d297796b46db733eee9616c8968ccba35f67e05cccae5fa9d87a66280c76fc115f67c798d8ec88aa23e3658c9f5a0649bccf1385e82a98114964e17abd7f431d1d5fba9a006707194b23ab7779bd476ba0eed496df92a9c61b20e9f5c65123b68e5a933e809f3406e9f1fe6afff9cbd5f26b7913f7d732bf7c61be5591edfa8e7e29ca42f7285f78d8d4335714ce668081f51323b8c8f8fd8ceff755f14f42caef2fefc28749db3fa2cac99c7370f0788c132e81992e7fb919d6748bd9fc8aa7ca9e03730bcc2d7037b6c072b1fbdf0a2a3d7043b8a968884a1089c020e6c4f21fe80d484b59caca423cb2005640a9bf6293c6847804d300b45900b72097ae20ac64702cfacce03a7581ad5050202e7d43343a631545c6fc4e08d11b45848720bea84835a84c6094c048fe892b48baf9b5a45f274957fc4648baf2f3f1cc5ba0adfdbced740000400049444154cc2e8369cbfac5da7bfc5bd09a253045a6ab0fa4d7d095d21ffcddfdb804262e7effec3ff40db76ea433d15fbcd353243d5d5e61fe1145b6fba13afcf85d701520ddb255b3dc5dfdfbccb026af8f70f475883ed67706d671e779fa42e5bcf4397e6e815bdd02182abe89322e3a5e6a7e602cd8e73dd9e9b9a235d847483a818cb58e148163dc76ec386620b28e237424778f27d31c55b8fe65fc26c6a228c582060176f41911130f4cb82ed6a3a8c6edeeeac1ab92f4f54e2e353e8f573bbf63ef64ef6f73cbf5ad14ff4da0c117e63b50cf51bd949ff927d702d4307521d5aa2897f668925e75e17bbfa348ba047db1cf4b4b0f30e88ccfd45f718b90f758d8b33c863c39fac1d20edf0c6c18add52a67776e81b905eed61678f4d1f77fd96ab1ff7ec1462c046630761438052c05c3905dd219d9598c0292792b5204d3a211424f503760e59715055488a7f4687710ea91379fb70bfdb34cc2927b8ab51a236f14c41f9f243e664ef352be918263f21828fdc9afdf781616c682636508538f9fa4f4efb588b38e2951dfb6a4bb26dde52ee66352b87c9625ddb45ef2827b325d973e5bd26d9deb3fdaaa6e8e9e94ce7821d23ed6af6fa7dffcb574357ee4f435bb83473a43f508bb4077aef49da43bb91aad8c1e5cfb16ddb4faa2da2a5bf7a3c8abc00e66bef8e33be462e00fa976733b5d127070caa22e51a7ffb8934d5bd4734b5eeb9996332dea37cb62665de6636e8167da028e3b965e7c186cc5b25cd85dbaf007601d03e0384ee601fc5eee3a961845f8cc33e6920264c25ed5ea14feafd566ce519763122dea44b672470df08fce1a808c4f9520e1504c990e456618f2b32ced758cc5335f5a47a6c5c9de5e1e49a2accfe3f30a72be9c825e665df38d2c834f59f6f90ed16946bed7284fb95c94e8e130de6a881d4619d477b25cfc21eeb2fe82e43c51d738f16cf9b8b2bf7f6d55ef874a6ff6dd367f9df9985b606e81cf8216589dec7f930014b01562347d883420a5605988a98724010b0812c004e2644a4a52073a15b9092e8b5e8af9277205f0cc5f30965c32220eb7e94a7904eb96a7805d6504b487ae35de0715a39cdc55afe8f18ba47682be40cfe44171584edfb1b3b7fb3a65ce3a8aa417465e41d259ead224ddfd5da6f901e02c7731ee3c926e5a2f7dd13f1fd76e815eaf3e95f4f66f878765fdf8f8f2f10fb05fdacf57977222e7b7b6eff2b12b5406fb47f9e3b35ff8a13319dffd31179a8433042ac1d4eaeb99890d7994d6f5052331a8fa68924e9d36b7d589bed2d83e64fd2e7d7b7b9abd2f50a671b37f6e81dbd50290f4afa7bf7f6159ab455589b8c84f6ca689807a2ccbfa7c8a4829931d35d97c202ed10ecc641297c17a5922d85f23b092f270b8fa19b892f29488acf619c3b1c61beb58e4dfbb61d27d35276ab57389e7acfec6d5483a6f06bd776f71f247b9c0ff1f907b2b16a75752d2cb72e1e15a1c8c3b19f1d4213399d520c6ea66a98ee09076b02d8cf47b5237e4ad8fc9b988704e3dd97937f18f9c45d219f35963deaeaa3cb4b89f5a0e33368299629f7237031b66a26e4bcec7dc0277790bbcfffdef7f2e38f4170296853a03f6ac381121ba028f6825501a8f1f704a8c2e9126ab43ab632488303d98963c0402d4bad14c3a9e52828fbcc8f93167f49196875bd15fcb642cd35c49e01ca4b7386312122c1bcc03bc81d2d562b9da79c7feeee27b9a8027d3e48491735831b26f7a08555bd7f724e93c3caa38f95997be7978f47a48ba96f44951b3f7065aa0c97a5b98a70f97badfb0076bbc9f62c2fd1698f961083704dd7e945ec1c4e951fd08bfffe95cf629849c4c89cb0568ae06d3fbe843f632fff8cdf92853ef07205e1d191bc95ea4de4256ab3388fa11fde64a43dac49a3efadde96dd954e744dcdfdbf07ccc2d70bb5b808b595e705423417b8aa342840f8115af3321d438714cc47ce39c2061c5f56f9739c13cb1342b3fc65693fad05dc7292c58848f753c634cfde4234b48703c8e5b958d32f4523f2df72c73f9301706ff0d51ff86cff6c1b29393df8fd8ab798fc25b11fe6ebed757a2853718504ea42dc89af271e2b21c4f7e7fbe4bc183df5009feac48c46d15eb658a247fcc51fa56ab1f504b1f5c8f5f909837396fb7d3cf747be7c6adc49b810d3351df6ad43938b7c0ddd80200c5d78331cfd322300cdb01be06c898101090630b4682568e8037e1c437289b02f405ecb49a946cc13849a0823105bc022d11e41f9a877c4a29a04c252a5c79044fcb34af051b163013252c8e3fd153b5d64b2c5dbd7b7ff70492be73a645b3087991a926f24dd2972e779990745e65bf7e78f47a48bad5e8dda1f4cfc78db7c0f9647d97f5eaf5b015fdf87dfcfaafb3cb396de6b7b728fa07bdc518bcf61b5c3f082ae7d9ed1b2390ce489ce97656f3c487247176d6b81642dfac5d66d0803fc9bb8b8f295587fc9b0ffffd2069a75cafdbdffb7ae567b9b9056e660bb025e31f04075f1673368a1d0bd5d1edfb195184c15a4056c743bcf54dd6be77c03116238ee90c9c4c0b6a5188ecbccd3371106c0ee702c7197114d27752534e709e38fe32a7a88bfc6d202223f957ef25cf5f41e7636aeb83fcff39c9df8ec49bd999ecfbb86bfb726421e7e649bed4d9fae422dfa592fc89153521e2c71b693ca1e9b87e0599bbade05b8df3fd8203c4f87d49e1ef3dd4f7df4ac69ba0af5617f7883bf5f17d795723ecdb97f04dd0678b7affcab33bb7c0e7780b001a7f5550f1109b80ab80500028f1814da3935608a55ff022ad501660039606a205a600dd721124afb0156caf4022cd1be05559f28e52056bc339bc48203e24ab987ed7570909555c91db1275f8585e6ab85abd679fadb9ae4ed211e5b882a46349df1b6bd2cb900e497ffae9ac2b3e8ba4ab63ba26dd705bd3e7652fb6c6333f9ab4f624b5b1acef2e1f1f647d7fffc20ff2bbbfd79f3e249c3e51db8ddaf7ec113002fa5c8879aa62df4e77a1fbd9694c27066f93f7ac7725bab601b5bf96c52c9374064cf56badeda8fe8ffd0d7b95e9309777f419eef9af109f2dea6734d71c75db5a0092ce0603767e8ad41c8e6797a0a434fc3c71463b6018070e2671da28e683b2301b724c85e20e19e56b398b84dc7cb913ab5e7439142926f96b4e5131612d49247a813c4ac9f825e94d14fa4ac6e62723b8b7f81db8df82ccbfa00e3f44ee3f86e0f3a23854bbe6112b9af5ee6ab3887c454bb64e248e7ac48095ef5869f9be7a5d8f93ca16b638df99495115e27e9f04bc887911742edaf7a61f2491bbc89ee9d90ccae015c70b0653ef07ea15b859d8e0579e8fb905e616b88b5be0831ffce0ef034e5e222a363106a2c41aa12b601382031835e88adb0a1418099a25df20286f17f4025ae29687322923aa036e8583294581fc2b2ae4f92f505a9706ccbcc90e252471a8d88a983ec8bed92a2af99442c37b980e5ecd937be758d2c772977a3d7d64d6967449fa599674def97e1e499ff74ab7d56fdfd113d72e0f603efe782cebc74c8adf04e93eac8e624762d91355aade8487be58eb60ab9ee96fe943f6457b577554bb929d3cbc5d02913e47bf9370106fbfb75bd740a83c909a107566e2a3de8f7fbae885baa58fedec1c4e9e71a8add97eeb8cc9b82f4eacca7ccc2d703b5be05deffae92fa3737f8d653a2e6a6c14eae6a2d6a130f03873002c176ecef8627c88dd8e335dc68a23abd6946b9176084a9473d96c4845eb79c479286acc45fe0a11035b0ef9374704485bf1eeb09dd5eb7676771f24faf722fcd728fb9f51911fa3bc3fc3e70b336e95f750b7f5e2cf3a3a7c4b7fe974898eb13e2b85005554c2ef62e6641f3a8c452ef5a8ef64a5fdfe2d4bfe9f60fe609bd6d515c4bcb4d559d23e0d6ffc7501af11e21367ec1275b3b06126ea9b169f7d730bdc9d2d7072f22d01c6e0922823707104a40c02a623ae0188a49213f0b42880bc01c3e4332b191401880563c9b3c4268a4b79c291122c8d0bc2751ec243aef45478d8cd4bdecc5448d0b7cea937ae061e2da8790869b5780f13c5aba94776d450747a6cd6a41fb1a51e6f1ce5b83a49c7927ee9d24cd2a78d789bfd3d39e9fab1f8de0541bf649ddd153e447ffb2ec3bd2eddee647ff4cfbed6b7e5abdf3821bb4c8b49da7e6f3ece211ba584d0588f2a4927e424adf93c3a479eeaf73beb35ea07c3947e9645fdb4edac26e47396a1de94b70ffa35e6636e811b6901de0ef17dcaa7af07cb1d431de39819e380b120e41b56dac37117cc27ce65217561ac90e3c7b0103ff225474a09c9757c65bc261e3902d11ab0b71c3e9934760ef1f2a0e6e2f74176dd4ef10748fc5a84bfd8ac5add2b8be37ae848b9d1960b70ade996668c1705839f47dec8d431ca3c39e689618ed9d5f48e7c5d486858470709baf5b7e25999d50fb4e5dcdc1e5c38ecfba9d0b5ce636ff57101bffd20a9b91b03afa5e96ae93351bf5aebcc69730bdce11678ecb1c75ec40e287f320443a0097e093e02ad95c3c33f4bf6c0ac02cf54b9011021920b48950f48990569d939871244c717401c650890d3fd67232d698f649d12271922321381d1049c101af82dabf2e2333ff2d965807d6b11fb8eddd5ee99247d79eac1d16b93f4a77bb90b5b7af93223aab2dedd45ff6c49b7156ecfd164ddd2fa95da4e625c640d2bf52759eff9d43fa69fbcdbeed65631ba4a2e1a3369d339ec3b769eea9278ec64767c04f4ea4f9aeb4f2dac06081efb9a116ae02f3a5c0e635fdfd92c7d4142925e1675af29725dc178babcbc87d0f6f15b6730f59bb10675bb9c393cb7c0b55ac0b5e98c9baf69f22d86a7cb572f4f764745c53b6688625ed08d7748c05cc7b871aca8043764ddf1a62cb1c9308c2ec9ddb55359846a458a795320e59ab45a5cc4f7723e5fc95c72b1caad7214736c47b755b3308f389ef8245d45041ddbfce72be8259c0f0a821fa3c064273d1679f5e5765be5b764e7a5882e76fe3762f3bc4a93735c877df651bf1659ef17a021bf586ce14293f39b850d33514f2bcfa7b905eece16383e3efe4bdc4a04e004bec06801d9009b20174049103222482606d931b40d9318aa6c94c44679e29509a8e95722c8491920a1f1ae73e41fbfb64b3da31c62c4f64491a29522c0677e4994fa284b9112b3eed4a327894a781380fe1ac010421d4e8df4c6d592cedb2c53c6996bd2b796bb84a4df27319f497a1aed2e383559ffeaaffeea18a77bbdba64fdf1c79bb0ef781bfc52faa79dd88e42ff306cb03a9e1dd13e6c829d473f7ff6ebf42b4f44dbff741907cb100d03e6b24febaa93e5368be34cce6b2b3addacfc6efa33d9f8e7b4499d348eed27c688ea4939e9f3696e81dbd0021240ace03fec787144d8efabb71bb6c71be61ce0c52fe647ced4c6e392725ed1a71ec5ebe0a236f38191c6d458cb1cb4964974d2e5c205fddc0923dd4f866aeaa00ef223a42a4bb3365e3ca708d31243c4d05d639f40572a325ac55160a449f1e30e31b3f6376f3dc6240e85ce659591ba2e169f6215fa3f968cf35993f3886c4e6758d6a797ef9b652f2eeb335b2ff3d37fb3d6a7ab6b26eab6c27ccc2d7017b6800f38422cbe515c1a26c1e04ff82ee8140012b3068885a704e12a2e8fd3911e3c4b227a0c4706001bf9d41d10035905847a104f1d360a202a0272545a620a9449b02ea91b8af3601f794ade82465946e0c7721e3d287e90f52fffa0021b725e860ce4963b79c60fa6ce83a3d76f49df83a497ced992deed70a7dd26eb4d667b226bb27ee9c2a50fd1fd5e63af72a24f47d171e64fff249ebedbeb6895b363a68fd9a7f98f78faa99d91b45c68d2d72287bcba38bcdce4198e4fd3cfbad3257eb074fc5ad30f9326473fcdd3eba526d6bb6f6ff777ba5956b3aacc7c9e5be0da2df0c0030f7c2b38fe920c0071d8b110c0cdace06848d873fc8ca5305a55eb1f4e30defc8e9bf1678edcc19549336a4adcb1b6d16951f9e0447f062401424bd28c5363aa95e238f99fbae292903a276ee8528eb1abaafa3ac8d42464069593e6f8dee85dd7bbeb46c6889638f27c0795954214e0adbafd306f37f54eee752e7149f1bc81b45e7e56a13ab7317d1b171a1fa6b2cfd43f66ce679a7dce37b7c0dc02b7aa05eeb9e79e3f05527d91c0e59b3b853e412620b74623c16c808f0405bf21d14ac21d4ba47ef12a1a4c4247c8cbd0175da45390b6745dcbe2849fb0a087ff4425a814604b26c5b8663c4da05e05151fac2b24cb92fde47260b5781d2f7b7cbd229b23771a13ece52edad2774f91f4cbd9ed65fbc1d1b6a4f71b265df2c243a4cbcffbbc27978b8fcccb5d366d7c677c6d556ad75a4cd7abef62597feae253ff986e3396c0480eec7ff445bce953f6c7f4575cba58266ae3d2ddec98f6d311ce95232a70bdc56dbfb37b4b10e23f39a91d27d6776f881eb7722ec79a9eed9a17f7f0b77d8c07614f454fbfd7a9843930b7c02d6a81b7bded6d2fa25bbf3af300e344800e01a63c4641fa7b668b10edf4faaac9180389713878383778fb532f1fb15e370f84122f31ce319cba081e32e8373a49ce09786a8e724056068b4c51eac9bf42463a5e718d8b63cdd1312eaacd431275d3877fb84ae50b9a7fa84a518a9568f2a58ce4343ab982114651ce877797ab378de4eb72bc403f64e26ae1a3bc95b42edefb265b1b2194b9d917eff50b75e9b33bb7c0dc02774d0bf090cb5f4e65324a0781093a152605970278861102e48c6b6b48b0cb6529d26f404d8be5802ce2f099d702044b9cc2526200c50a577a816f09062f0553fe8d4f190076eb157b734430a904715972c05be05e05ab9fbc2e5ae3a524fd9807459747606159c461e91b4bfa31260c49fadea9dd5d5812b4ec35e96d496f92fee4e77ddef22390f4c3171cda68eb378d7a8762de7ed116b97dc77956a5e97af507a80ecbadbe993ef229bb0afddefe529dd28e667f5cf7a7743cfa2a933af9ec61f6c2fcd0c4859bd09175d33b8750e41c00bb3b8f9ff5ed7959d6d1855a9e3e929f5e70f19718ac68f4cbc1efb732fbfd6ef6a4bc55c41c9c5be0540beced2d5e4b87bfcf21e247380efedabd917464d43246fcf4f9e0378265c546c8511376ab839fb4acdbc69fd424a349e589a300c79389290c8f7e09beae13478d36ce1cbccab4e696317324d204eba0222f23cce108cd284db80a213e8358f1ca9f8b0507b875ae02929b135fd8b9c7e8a1c9b14f44e343d5b12ab062dd8cb29c7ec8896677b93cf0c3dc73c0f2ca03e61fed43a70ebec7b8fbf63497ee9b7b6ce311d285bbbdb8ece5565ad3ad90bfeb7ccc2d30b7c05dd602b525e3e2ab424700a95d0032445ab062d40a39b16e04bb0429c109ba0e18898559372e7885b138ccf12324a10f900d793509c005600564b9bd283253a031016832677d6064291bb7d346e9c8ab85582707bd7e08a3e0129f57029e3f1be3e57191f3beebb8dcdd71950bb71511e7d3241d329efb8cee915e96f443adea4b493a922151db96f4279fd492fe91c50b5ef08293de2bdd5acc24dd56b8734713da26eedbebd52f5c38faf59d93dd57a5b3a54fdb59e94776c31001ea4e7f9200a43fd23f49cd178a6b871b137c68bb79ed7b44e7a13293c98873a95ac13eb839ec4c5e107279b7893ce5f372e2ca63b6a85fd92673ccad6b81871e7a88dd5376febcd8db74170308cb0ae9eb8e093ab8d06d287754d7e416830df10e807c38994579ef90aa2bcf3519eeb42871be982895ec1aaf12a3c93b465aca4c0ae32cf31682e32eaa354b5ce626c669543890bd7826a0d77ab8138d843ae996621996e09c17bf696c3189a2fa3e24f8bdfc069d29fa1036c234c356b6f2bf676fefe461738c23401086cedc2371ef04dcf51239eab63c3aaa0bf74a2f6b7a2f7bb995d674cb73069f8fb905e616b8cb5a00707cb5c824b688b51a328a3613a3c5d143502bf0c18fc724dc5a1a603089394fc98efa1ab8946980332e803974c7b10ce4236791467a4e1c5e32244fea627d2a2d255897e5c9c7a9ee371f2f4e7e6e710ceee5bf5cc3f0ee23c9b91cdd835b8c241e91749ca822e9c72e674176ff6c927ee95296bb84a4a3634ad2db82de6e0a994f77a405ce22eb56c4dbca3e5cfad4e5a7de0009798793ab643b7dd4bec6c46ebf4abf463e84db6833dbdff4d849f9af48dc7155d93b1bf5cbb8d0c51a757aa37ba8e7e89e578f91ae56bd5bd0e63677c99d7deeef7476ea1c3bb7c0cd6d01a8ec0fdbcdd39f876ac78a1664a970c68080ec84a1b53963c3b133eeaa2a37e2921d7f5dd836ed57057f91210de1ba9b65a9863d934ed039c173d6919310038dc9a6289e2c6d2422601e974992d9bfe8e2a9d3e85716753bd439653bc7a5ae959e792565f95df96ae68f2ad271934e42ede8a2323eed58b78ab904e1fe7b1884f661e3fb8c7c3feef8b4cfbe0521ec277be3ae2ef17d884fedd7bd98652fb5cdec2726d6f496b91598602bcec7dc02730bdc452d106bfaceeae5564940eba381d170887792829c08161495b4e73625b43f992a3e0859604b0459a73a806db208fba5d3344a46e664583fcc23ca4a828c131c5dbf2e8152b6e591f8302fb9f84648d1078aa06f4c14b0711e3adad9b024c258330649af78ade81e70f4c5fefed9241dabf9f1e1bdf72ecf23e9fdc651d4cfc71d6e81b63eb76b75fa9671570d6bd7b7433adc7b7974257cf6471d3a663d546a947ff43f19835d1c815ac76eb6220135b99ba440c9d0699fa880e74df773af97dca2d9246ef9364b5fa6d63385fa2ec156863938b7c04d6d81871e7adb7f07eebf2c2cd93e9e8eefc0a07f83c15aa47533580649b7e73b573806320c20c0ba12fb9275ac98965123d2f367924499ff3044483c2e17d17dfd9b0b81bc0d5859fe72e110c26cf1ea241645d8be534b953a4fa0246d92f2d4cf5f8f5713328510975ce657a7b4dcfaaac98a78a04b592f0eead0edefa0affe34bb5789c4ed9efccfcc31bf3132b03d6cf9722717ef060d2a9eefe034c5f134777bcfb7a6974ce1402f83bbd99830be751735bb730bcc2d70a75b00907c75a12ae022e05121c9b450d504bde08db00016c4c2afa4ffc950f1f18bccfc17cfd62f10222723d7f54fa29d48320ba6661c4ec030e54bd691b31065cb97ba59a40aac9fe41d7d3f0fccfe152ce6ac16bff200784fe1a22b5a8c383e1e243d145d13bc24fdf84c4bba24fddeab9074f3ce96745be1ee38b6272ff7579f2e81d17275e9d2d187e83cffc80e9b2e66ffa69fdabf8a4ce0212a6b5df1d8834d732dae39cc537d9170bab77d91c89180f713d5ab4e75bf6b90f4f356a8dffc87c6a8e97ccc2d70450b3cfc96b77c01bdfc35190962b163804edd7ddc0c2bb159ac775c747f47c03c19039e3260c65c620a71be00c83924e3463d2a8bfe0ad4f4621e642c43d9e91218059c4b285bf59e24f11e3a2669354faa7e22ad5f93f9cc2fea543e1aea7bf9fdc2c395e753c6a0ca2f61afaf58dfa10b96d0e72dd87e27db02a5a9cbc9ce23ac9dffe779610204dd9d7fa708c0d2cbe3f13285d49b53e61e3169fa10e9d83b7d71c04bdb7c13a98686e985fbadb0a65ba199a8f7cf32bb730bdc052d8035fdf7802d2f2fd812740a980a73042c2b492447c9e8e970c596cc488d3c7e0056b0cc9f7e11ccb0e9014d5cc201465205d758404629ca952af2702350b02e15f5ea77c97d5d54789770f593bc7efa9590fa8d19b270cf0a528fe9edc52c7f092836790f471ffba4e387a4ef5fb126fd5a247db6a4a7a9efca534f66d3fdd5ada893627df65e4b5ffa68faa809cceae9b7a3afd6ec4f2fcb05a1fd30942513b764ddaee998f0c2d39eee2ce738aa63f752dde41ee6b48e9eb8972f5f3e75abdba407263d79223a5bd3a78d31fb6f590b1c5dbcf07d8c81e715d48bdbce05d5c7573ebc69c9f6f160ba210274fab68c3320f0bbbadb24e24d5607c15cde7a314c78e44c3c46f98c3bd31d534844efda629e328ce78f252c64372647341b91184625ba320625d87aa8485d542022e9277eac568b9eb2f8234745d5a9555d01fdb9834b9dfc2ee649f9398ff9cb622d3be5db36dea15b7d0ffba6e7b17052d72cbd5140d2be77e2bce4bd35b36eace97ba7ace93591f58bcf9aa437a69977db2061dc677ae477fb4c95ccf9e716985be066b5c0c96bc4b18224750a8f1d2aa88cc5da245935605498848c7e815a622358995ea68b683028e511fc04bbb694cb4a62b1c023809312bd593ba81c418f8e8f2f24488264bae780f12120fce06ab5fcde9393cb79db68dd3bacb36bd2775c934ef0d8bfac51f78547941f0b7bd6a6af777791a4bbe485e4f1e0e8a5a327164f2cae45d2d5375bd26d85bbf7e889ad27b5e91218b66f7c8a59f8bb9dc8431eec94e9c1f435fb337ebba41f7d99f0e9f792894aa03f22d76b5fa363900ee21e7717f53c1881f8677af4adeecf54cf9c7f6e81f35ac00748e9dadfe02c90b9a0d6a3d0c709f3c9c5e9180d8e888c8a311e9c4b6a7cd41c2296fb3222f1be083b6ebc5ad6071d24ec056eee609518feaa5d0c34fa89cf698c2b777a29d6ec782c594974bc4c3c550f4394639e24a47216c651919613ab38738ad529bb39c9f93ee846aec979e632621cdf7e6dd5e010530fc7ea1acb5cf746743e8619a07839747cbdec0509232ba1483ac1cc491a0d7676ee5d5fb4afd7a66fedf4827c8ec6b40edf4c7726ea37b335675d730b7c062df0c10f3ef27bb03cfc31a126560300481072e79690f341bc631947d0a01825510e61160d452b094ec04f3dc238d1c1324ec0ce328c4631614f70538915b73c0052dd6450b7f5301df2ed83a111b34e2931026228e9272797887ff5eee5c5eb0be68a8c4bce6ba901e51eec1c898041414e80272b5d8e8f20e4d901cf40edee824c487aefee7249727e74e912bbbff0992e777962b2bb8be47cb6a4fb3bdefdc7749dbab5dd5e0273f9a9cb6f806cbf379dd0feeac5a73dd1aeaa792e93308efd96c84cd09c9ce8435e46bf4fdf46c489de3c647dc2fed7963493faa0ff9274e5ae2ff4c5630dea4edc7d41d11718edb68ed99d5be066b6c0c30f3ffc003dfa9ff1c95ffaf9406dfb76116e4ab4df9713526c20461b1272c14a5a88b76360083b96d42709ccb8628c48e23dc67009698e75fbd490a3dc54c49c7ac8ef43a04e32c89996398ab475d9fa1d9cfc5bc4f0122e0a9a34d79f97bac8441dac3ca37be8cdc57bf27b61a132d275108eed3c15777e52976db6f391fdfdc5ff6a1d3d18c3e1e8bdc39871f0f6e3e943a4850364df3964a7977ac15191f4b1d3cbd838fd7659d3536f4ff331b7c0dc0277be0596abddef0c887112e08a6440900539aa27410ea00a4ea3baa1da24261d1692fc9c132f821923e986a10b7c86b365a37ec12feb587c9b1c117ef85faa2de99647594b5c6f8d9a9b0a04b38957dcdaa1f7a3247c334b5d7e369672e442cec3c809702c79db681378c392728eb56153d2eeb3a3871025e2c79af4132ce992f4c55113f85324fd89274e5e78e992d5595bd0674bbaad71f71f4d70a784bd27be5afe82356bb17a5591047cf9956bb2b75bdb0b1d2346e782330323811a0bd55b335e72eab1b03819db338e36f29ef7a9d5aa237ee2c09442d227516bef6c515f37c5ecb9052d70f9e8f08d60ee17079ebb67d3ff83ec027230db5062e8f54406ef0907a315f2887048adfe0a55740d1dc718f190df1a5f48e1312d658f78fdc99c848cbe31c67af98de53ab59017d1ca3bca21606ce253963e74c0a993e255027aa3daef8347ebbb833f75e294521053d4a47c6d4fd64fd18ac00755af2ff27d2727bbb9bbabb84786fcf44a7dea676a52667797e56f9b6dd38dcae14bcf7a6dba118d63ed96d4cd3fd344f331b7c0dc0277ba051e616d3a68f435013648b1166dfef968012970abb310547094b06835c0ccef90076e24ec805cfc809cf2225d2cdf1420109e9c40b12d8c78ceb8155f1703e409de9a627e348c7c29539de45fca9e4e568f40c3bf61b95a7d4035db07fcfb08125e7468983164e8c6b76c2ceb461eef2d574faf2eef5fbefc14a4ff3204fdea24fd852f0c56cf96f46ec9cf3ed7096e4ad6db62ed37e105b33f8741edc7db12e865a1fd51eb9b13576ee92b4837f55256035b5d84daaf4d2082c3d192fe4c3ea6e08f27b24f0707c7ecdbbfee8b1dddaeb77ace599e9eb70fdeea09baeb31bbcfbe1678fbdbdffe77e8bc2fb71ba7eb0adc1cc236b89b0bd5e036c124111fcbb4327c720ac607b523140d115e53e608666490c174b3940f781df91376ae40d0a527153fea8574c97be14c59a68f0a39f6aa7494122781c7830c6e64083ad159b2c5e11bc6f004326e89cc9d0345c258ab1e56cdac291bb78faa7fe6cd7730b7fc5cc7af5d89f966c41fd7daf4a41e33ff85a8936f8975284b2ed98d11e9fb8ff366e2b1717a1b15cc753b2ed667a29edf673ecd2d70675b607779fc9d424b404c380b9e15fa182f0016394146c0030d05244f2e4f81280fdc234ce6c24194087ea299327e45fc92f1a827cdbc7992bfdd82cac815e94796ecd9ed0519f5a57c75ad5600e1f2afb26dd7995ca6c838a808ce097545c87720e99b1d5fd8ede5e950f4bdbd58d143d0d55dc7da92fee94f7f7abd05e3135ad227245dd1d9923e5aecb3d8396b171866edef617336f63fb6cfd6366db1a0331eecc319037e67bbbafd93234441f98c11c78dd635d31857cbe5c6a29ec97a33639bb717be7007e7eabb3522db1719e69b8fb9056e660bfcd44ffdd41fa7277fa758ebb6a4c160fa782dfda87e9e8e5e1305454b7ead016e2a32641c189149a40af014ad0687d7b2c6e6254223bfba42bf6b30396d7038e6988df4bb4c25a18a774ec9521463ad337f31909b3c8e525169ea42643d7e5345be642ebe29c069a6f4235f95494cf64f372f717ed322e9f9e2c6e6a8bcab8f3086bf7f44ad1de697f55ddc75e4d603a4b1a66f12174ff66b4821e9b7e32da493a2d7de99a8af9b62f6cc2d70675ae0973ef84b5f0eb67d4d0cd454412c6d60cc6d3f812ae065827514a20433e83bf1012c44620d27dd35e645c65d98c21f2818128362c14cfda6c3be87ae9a084cb3bc7cd42b18b236d88b00a13059d48720e4ff47487fcde9db8a1bd203f98e1d2227f242dadb5231842efbb2a3dc5c0450d724dd2a8ce31449bfe79e7b729dc1d297e329499fd7a477737df6ba4d78cfda05861d583ec25670ec783126e3ac690d0db0f7d33fed8c7cf7745ebab4337ea22a520b9e6fe93559a2cf73124ff0caf0ee965734dac52b62ce8f98de09385f6a4e995be0c65ae09def7ceb97eeeeedfe78faac6bb425b074e75895edc926180f10679e304cef76288494dbf5c37c1505fb332718958424d51212528d42469d8eaa3a8fd146a15d26a505f7539459389237154b28d386154d6ee60d679728568f058c0f9aaa24a2cac26ef9a5dfad15fbb0369b5cfaf9be246bd14fb124aa497ff9ea1b1075086630379d9c5af212bd5bcb5c987bc67c542f3a73d9dde1e1e62da45ad3ef1fd67449ba3a6eb735dd3267a26e2bccc7dc0277b005f68e765e13440ad216340990799db2c027f156407f300dbf888555c3b0d646f9b56025b9d6cbca144282b47a9218600f991714f9631170805eb22f192feb7ae992a057b81a46ff89ebd94f5697962727ae1dfeb1ba7f08ce65494bdf4dccce2d1bc64e76d7b9a8454bbaaf6907fb586eb057247dac47d7925e25e57c2e493755eb792f77992de99356fb2cf63659efa524d32530cba3e583bb7b3b1f711a764ef6252861090e95788d757c103d5c098c7dd6142f5c3de8d187bbac5765cfe41ca73a6945b1dc6678aec3e93a5f87e82c32b7c075b5c05bdef296fb588af52fe8e2cf4be7b53f73e4ee50df51b297dbc1116a726ebf97383b5324ce3c7e90aba52a48643c3016942b86ce9801ef25c766c2715ef050bd73cbc88c7f906d75e0673688acf23d2ed5a1da9a81cc8c3f4e6a45b289481b17fde5579b5a4cd0750eab38e594213d9551a37533b3f52c45918d573da62d7e94bb10ef8bf0e4b4654dcf9c541b8a5d465d2f79395c9eba9b767f3d40da6a1a971aa7daedf45be5ce44fd56b5ecac776e81eb68815ffaa55ffa72ee6c7e6df6a1051dc59f906201c770412f2027004135122744f1571b9a176691860447c98961d91d43fc236f6da1485cfbe52ef8436200ea8038baf94ff972fb601f6ead755776f501eaf617303afc6cd372d7fae5e5119275fe597d7e8aff68493fda819af3c13abad839da39c66a1190c4eabee4095278ff8d9174bfa596f499a4db129f1bc7b6755aab9593a2162efac7211df4fb77601cbb7c76bced4e3f65affefaa4b393960e4b7b48186c16277efab8a424fd7b4cf6d30e3af523d31794b19c9dd5b2dbd6b4db35519f559739ee73af050e0e0e78d9d7eecbc4f2186b5ca44d67b7ebc6b20e4007fb65bbce05959c8610bbb594e72e93386f8424371aca76ad445dc03a2ed4ec78b230c60861751b51c4d079c17945018bd33084df79c3f2c9c039e9994e0c99acaee430517d4a1157570c64b72ccb55bf82a5c7ef1bc951e7563244a23b16f78c7bb3592ad1a31eea61ba7c2fb8f0faf14c68d23d85a4af43e539cb9a3e7d7eb4d6a62f16ae4d1787c4a3edf1bfa5f2960567a27ecb9a76563cb7c0b55b8001f877b2fda2c02ad004b9f0894e21d0829e74dd24220533c933028129c058836108bcc02d380b845ae19545aae5ba1c0482693ab1a42310326fc420fc82f9d22d19f9cb84b13cf989d5ce326f1a0d2787e10c6e1e82aeb5dc4f7d639da385fbba2073945dd0b197937e8aa4bbe4e55a24ddf5e82e77516f13f399a4572b7f2e9d9bf04e097b4f8a4e92172fdef74fe9881fb06fbbfa654fd2ce70d1dae6218997848461480c3a3e533f7d58f2902bdb88af4fa7ee84dfc8ba1734749dd7ca66cfdc029f410b3cf4b6b77d23ddf4cf07e645de74e922b1e27288ac71ce0bfce5b0abdbd7f937c9bc991f7489c0192360906c648d577097b92264d94143b82cd7ea266c3ed7c69bdb3cfcedea86ac3b271163414357ace89637ea6242e9ab3aa4ce9651b351b249fc33bb119fefea18a5c2a9337ed3520f9c22f116a64c7dffaa95f54bf5495b7dfc646ff19a64aab79a95f7cab3f6a13157adf200a9ebd277787abd5f6e1492be654d6f3cea71dfee95ea6f7e4cfd22375fefac716e81b905aed102ef7bfffbfe2084fb6bb284458402988a68e3937073845b48ba4d5506205c4ac8638d28577413e81ac06291afec6e8b288b0f804ae6ab8ca11f1d095b2e4949477ff64c272ee47da53573f13d94fd03e8ba72cd9f750c411fb847b8b65eccb62eebc890f4a3dd63971668493f8fa4fbd0a81fd7a49ff5d0e84cd269e0cfe1c3c96f4ad6fb56b3afecde5ded7eaf84dc3fd9bae47b84d222b1b84332e40a656da77737a12f89a7792e62dd278df262737ddcc0b297759ed933b7c04d6881871efaa9df071aff43558578dbc5bd280d367bd6124e7f169f1194a42251a45c88e7639c9fda4c40430e21c689281f4bbb7908845f7b57aaae7493cb24e721dd9a67a29e3065500fe7144b7070594a2a834fdd8870a284588c2cd33cd4d31ca6e943b7db25fa0652c32335f9934e0c461be4fdcec4a4bcd26d895546be78555bb5061deca984652cfe1e1871c5c6065bd6f4b1e445052e79a9b5e98ba7ef710fe0f5f2cb7e80746a4d3747937331caf0ed3a66a27ebb5a7a2e676e81ad16c062f1f7455876380730c01b9058901660ddd7bce2028b01c63c1847dafad56e629340459c6b11f589e26a504fddbfc72a1ea04d227ba22bad3aa49415e4f00b8cd1a4eb8c00682e97ab0fa3f71b8e4f4e7edadca70ee18ee743d9c3a5b67461fd8bc673487803e19a0211f5f4d12eef35da3b5e5eb870e9f2164957ee68ef89278e25e8963193745b613ea6bbc0b877f1bdf7dfff7f30613de2ac9d5131c8ba3377ad5b7762679a27a0a35426380982cdb9a2839e32a113b71d5e37fba7d7bef33cb77bb23eaf1e73fc67770bb02efd0bc0e837d2412f1606d379c1657b2d70cc01be07a889279c3eae671cca550fd7359f47f934de648a2058f1a8c0e3bc21cc07fbd5a55f41fe1d3bce0146499e9d51cc1c79b3386710a78ce51a2f3177d717f3a6d266a6a0d42bf206f5f0319e74af1342db25f9a464cc3aff18e0a825349491fa18838780f35965200dbfdc9f7d9ddec0b8ff374a4d8f2d929ea4b396bc1cde7bb85eee560f901e673b46efe699a9ade953ddb7d31f1cbb9d05ce65cd2d30b740d658ff45c8f2cb42b40126a02747012b4006816f0bb9c825918e952258471a6e28392829e1d6a2927431cc3800b6ac1aea11f90456710ebff230762f0c8461497b16b9e0922df278df41be6f00a51eab9a8599e32d372f36829b4bcec3b43943da4f917496a5cbdbc743a37b4bd6a8b3d4e5e2e5939327db7211828ee5fcf8d307075727e958307ae94bd5673e7faeb6405bd5b77781f1fbb283cbdff555e7ebf5ea4cf5f58642a632bb763185f8a50939480ab9d95b1ccacbf3e1d4fe12da3e3f673be28a705bd7ae489823e616b8ce1678f39bdffcdc8383fd9fa2eb7e51755db15a120b6ec7076e8bed21a9460c3c37112c1fd2893747e75a5be5cd2291553078df5a4d206a94a326a789b27853a02c5a512b15624d0de29738fb319a38d30c7044c7a85fed775ef54b7af2b8fcc6c158f589959ebcdafc9d9694b30eeeeba85315c275dea23eb643acf649349d7ff5ae568fec2d7658db3f3d7a2a9ac62dc69217a79f0bb5e48539e9903790f692179ea63a5a64c9cb03eb8c7d57af71c984db3df669b5f9985b606e81dbd902ef7ffffb9f0bc4fcdd10e92093945bf491287b3b71f80522fd9129f25d966fade4448bbefce790b81b19ba5bf109cbce25e3c84acb9783888b968157f39b6fa47371c00280d5eb8e8e17af618ff4a724e421e738813edde335374fd19e6afbc5cbae478f255d92cea2f564d1822e4967a1f165df60c4cb463d8e24e8dea7e401aa2c7539d7922e4967494472cda767650b38597ee2139f58f2bcc25b98cf7d608c6eaa255debb9c49df93dfe9dc51e023b9ad82501120ed3f5afaa3ffaf073f5d2d34de91275e432824ea79c1d9a2dea67b7cb1c7b7d2d2049bf78f1c2cfd035bf4220178ae981fc8bd41c32d31c2e6791900e29450c0f0cd7a0d3f9cb4748026d6cd86d9251674625753c4bb871132d11268f7a35f298e6983299b822c495d769e7c4c4e8224019e6c517bf1706796f817989cd9f89d4339b17585fcb4a6acd7629c33a93577da8ce2773131a2cd346c9d237bf877953efd525eaf2eacc4a3d57d54cc59cc79ee99b2373510799472b7c0fcfbb1c6d96bc74fa74c9cbd49a3e25eb2d7b3b5c9b653ee616985be036b6c0f1f1e5bf09787d91502bfe78ac96c44898b93d28f006be0c0b7302a24982aa3144c4125f1018821eeb3b68e6329afc2b8b1ef3898bf910889c2e606c9c0289b3c493c547397fe3f1f1ee9bf7f70bfac43c17b304d5ca9d821f75f22da310a0cb076cbde8aabfda7e7143d28f8aa42f9721e972f427ee5b86a4e35d9374fdb526fd37a8d5fbd60f8e4a8666926eeb3cbb8e9e10db72d593a5649dd9faef847fef395333a93b3676b1a9e12f4b644de472755242369cf521015c841ee46f98d5d3ad9f69cb76dd9e69fe39dfb3b7059aa4d32fbfc249203b1911c8039a83a816391ee06f9cdd5a593de2baa77cba1d25b80a559ad89f63b892eecd31f481b675f796d92873015a19382befe8f2a7653ce3cbb41a68a304f3a10d9d45b221e1168d8cf39a47d5858be4f839396f95431c52ea4b391aa794e23c2a5d92eaae75f026fbbc8973566647b28a01487cdfde62f191c5fae151262e0edeef716a9e326e8fe753cadad37ba66b4ddf5c9867c9cbfd672f79693c52cf9d18f73351b7e5e7636e81dbd4026cc7f8226e17feb558c3852d2dd97e04225d802a80193f71d8f7124ffdc430a223b326de230f047bfd0d8275ca92a6555d6b4c102e0941ba48fb263af5b9dc852d5ede8105e21b4e8e4f3e202ddf2c355fabd5730afc5cd7e2c37858cbb33f3a1c5dabe6d321ee0a1f6949dfe5f93d48fa38f230df13c5fbdb926ed2a3f73e7a2c197b1fbbdfbef4a5653d97a4df0950ecbaceee9d6d814c8e6cc3d97da06f415fbc78f1ff64067fb793979f1d087bcdd9065c2bcb721866f1101a26fa58e9706bb23bd585c9a0797d1cd7b9eb4bd767b6a877c3cdee8db4c0c30fffcb07b4a49307922e6e6b3197aa8ad3f464fec56d619b889a1bc471c2f669b39824218e48c4c834122a9f63c2bc43c2e448c7933904c64b0c61e3a78e730ab11278654e64c45e281869194336731683abe62c2395c371de51347fcc4cd69924d565bc2a163d4650039c907ef29bdd23f522deef9c316d4afdc7350f06ab079178d73023259ffe3348fa71483a8f7f5e586c7679f1c5466d4ddf2c79196a701a6f7abc078f98933612b7cf57d875fbca9b4b9a5be059dd02cbd5d16b01b0fb449bc0218c5bccca6900d112448bb501645cffc1d86b550b42a060b6d522a3592b2fe028aa69293736ec1b37268882cd2cabc12b7117284fb0e2b3befd5368f86eb6697c0d71573c31af7a0ed9cd6038e595a49b409ec44bce25e9c6ed1d1d859caf564f3dd5245d821e920e924ad08ba47f223bbb3cfae8a3c718d163a998826103a43ae7e3d9d702eef0f375bcdcca6ffed297f240d70b6b6f75adeaabbd9dd78488c7ace6789198389dedd57297349716763c3bc465b789bd58d49334087afaa413f8056f855fc9d4bb9f579ecd79be88dcb4c5ecbbfe16f89790f4c3c37b1ea2bb7e85186caf15c3b3230ad81cec2622fd963463d2b3d38f236054e607e78d8c8148955f62ee9fa49aff3ac86b78a8238fd129a94a4f9818d9b2b2e61c32ca864443a8bd94d02a6fb999841423d170450d9d0cc35abbae1c0a505b7316e90495f7fb395dc5e0447259eb4db774b5399f995d7f59d543f2c958cfa4ac7e92256faf27717294357d12a177bd15a30c7bbdcb0b7e9664ae0d480a7a3cfef8ee55f74cbf5373d24cd4ebf799cf730bdcf216f8855ff885df0330b14fee802297bb506ac2c16042c14a418ab48164596f28d16081b970e987ac03e49013f992cf348de3634dba32e3e37ee90271858dc4bfb3f39e9de3e55fc492feced35f3e8f8a6a5737ba08ba8f8df2e9ad1729f3c88f024c16b5fc456148fa659e94e758836013749243d2cd730f64eb8927362f9068009c0990ad331fb6403f3c5c5b727edd65b87ac8fa8b5ef4a2c5c5bd8bef8597bf431293f5ea4cf07b0c914ce6c665422703f14efdc3aa7e696a40b78c98c7d63df5cafd19fb91322e42f3c213f3784c2f282b663ecf2d70f516d0927edfe1c587e98f5f110eaa355a26cc216ecb5e1312ab856849f7e8bf49a777975be9ee6b9e9c38456f25f5c1e3e4e5642832659c3148de2163528873a42cdb8824c6cddb4b336f9047733887f5ce91324b3c4598ec77b14e43c8376b7b7758529e785df5932135f54b5a2a4e6474eb8bf3fd7daec40b0eb775d4b52dea7b93e9e78f7757af53d3e9e30a6b7a2630eff45ebe802d7d7510439263596b7ae7d59a8ef5fccc5d5e7a5e6ab7f3dc6e7726eab7bbc5e7f29ec52d70f243926e712c78a40b190f8886690ba29269104b60f24f7073e5f989fbb27084bc036f926eff344948eab5b89b2dca71fb0f3d9274c15001e5d976f1106bc5eb50fa3779f5e3c7bc5d580bd1c53597bd90ff6875b4cf8743f61db62e21b70a4dd0f51749bf7cecbbd7918d35e21c929ef5e85a43fd3c3179cb5b83e04cd26dd1f938bf055ebaf0cf67197cf078efe4e03579b80c861e8ec0894938dbb5f5ca58a679d23c73ec72a108533f60b50bffe9cb98d2fde7d09a7ea545dd142776dde931f7d5696bccfe6bb58096f4cb87f73e0c3cf3d651fa6418f258ee22f683cf21a305f8026b2cd921cf9259049c39ca6552402ef3013100729695a40ece1bc17be4438c9d141817c825ac3c87da8682849c770c777c499184277e4973320cbd0ca8dca4aa4a8cbaab19a96430810f72ce6fce3f54abbe573c430fb2914cba19c7374e06832454057201b0bb5a7e98e9ee3bf717fb8c5f87705bd1af20e9a431e58d75e90cf18c773735c88b8d8635dd090e8e4edae666f2f69217c77a94ddc1d34cd4ef60e3cf453f7b5a006bfa9f0560beb2b6970a2a0150b26a70081a20ffde7c4c1fc006b9f616a0c43b67fd926d8565e523cd0b80e449c4908e4ce573bb467d287b64b57bf2e781ad378794e311a644b17c3835112f537a99d4cf22e9bbbb474f037cac53df5d1e41662434d722e9564120ec870367926e8bccc7d55a40cbfaf445572f7ce10be9f570800bbbbfc05af49f908448d025e4f4d3b8b212fd4ef221289c98ec3499a79bdbb725ec756ce6e17bd805e2d4f140dbd44fc5ce16f5d3cd3187aed202ee937e114b3ac8fa32097a969848a6ed98c2b83dd34380d6aa4e74586d3c2668bc31aa8c3c3e4cd947901e7997ceb84485fdc44b8fe58c3274cd3f3dbc28a80ff990adc9013f1e45b314c5f9457f67eefa20207f6ea5e6aa81a63487e58db858e295256f7d2f9223642ecbb50c62ac3feb6a5294ea860635a5a8845797563b7bdfb1bb7bb261d5d1e5b476c5c3a3bc558f979be542bcd6a52bba73efce72fa62a3cafec0b94b5e86fa3beecc44fd8eff0473053ed75be0577ff5e17b20b0dfebfa3e01a81cc1098011d40af5b8fcd76222e4d5ee2db1b6136ebe6d4e1f3ad2b65ea0ac64a52f6361d7f08714e0e70581cb675c06a35eca3924f78f702bf59b5897fe9122e9884b5b264793f48e92a0f3ef9eb39ba52e6ebd7874f4b46f19959cfbc028e9575d8f9edd3a50fad4534fe5a151f5cf24bd5b7976afd502db64bdadea3b3b7becabbe73d81643c940f92506359a42d1438036d3dd86a46f977ccfa2b9fa11db406ca77678b6a8774bcceed55a4092ce5ae877efad76b0a473ac2dc58dfdc681da9254ba675e5c47bf8dac7383382ef9ceda10d249abf708a82c5246c62fdf0ed1b69b1be5417ef3d4ce2ccaf37142314ff211957c2eabf16f24e1ba5ecc8b83ba039099a5f4903438bc42c9a3b8793d65bc8dd83c6b455c7487c023431b48fefd1b5537274256bad2702257319e1787ecc1fe2abecb6306ead09aee04d656f5114da424bd2ebf376f1fddddbdbc5c64f14bc9d52e2fe72f79694b7acf536bed77c0b341ae3b50f85ce4dc02cf8616f8d4a79ef7dd60de170b4e1e015f816a8095c4da8743f3903dc01a6bb98686a0141ed394756d4b0c105a56f000b86da197b8c7e2429c389ca24619e87d148063dbc5931fe382214bcfc3cf27249dbaac8978b377f21ccb544cabe52fbcd40892ce92bfe494a40b7e3c1bba5ee58b7eb398be5e8f2e49d78a2e4927fe8a87468d9b8fb905aeb7053ef6b18fedb6559d3efa2b8c937f29918965923193f5e95ad8f9ecb9238c4405b2c370e0655beeadac29fd000eb3772611bfd2e27665cd9cbc7b22bf32758e995b60b178eb3bdffaa57b077befa6fbbda44cd060290d130b76b867b712f81ffcd610435c007c08a4ef26313c562b78de28dd189f4902bd11673e8004eb4d399c7dc19069990f70bd4e202af346e5318db163c238c855bed483b225d6c44484fd1a1d69c94b65f5278cab212a65477014663ed9386a8a952ba3547fa88cfa9347d778ebeff744339f2c095a2c7e889d147faee7a68d8bea33f64bbf3066a483f5ba74b662749797c992175f6ce47ee92990d3747e32ee6e1ae33351ef5f6976e716b8052df0effedd7bbf1ca0fad6ec930ef848baff7ff6de05dab6acacefdc67ef73cebd75eb8910698ca1a010b4024112a49321868ed83176226db7c9a01346cc433ba3cd18dd1d47464751235641554c85e8200e1589d118892f0c31c843e9166ccb422120c174687090f04a1a791670a1eebde7de73f6eedfefffcdb9f73afb9cfba8e216dc7bcf9ae7ecb5e6e3fbbe39e7b7d69adf7f7d6baeb91cf40ab433c899c5af566c4949ca02e23be236dba1cbf10e62817d42f6a4338efb785441a16423cd7c07965fc00bffbf00a0df534c07b701e2ce7f31b47930ae7661fe264b2f669aba23e3b9c919417a01743de97cc4686feb3090be6f3efac5a6ba5c091e8beafce7777bcf3df77cf98befb9e76fde71c71ddd89fbf96dc055565bf7aaf72f9676af3a96fe1f031f765c0d22b3d139e9030ee89f663fc09dfd8cb9ecce47dfc4b0cf37f77d0cc56ba75d002ba5dcb08a1e888d1ef5032a1933061a78ddeb5ef79ccdddd9db39ff6ef7bc0c50cec673d3e1b97993630d84ba2d04b412d7c32e1dd1ac73ae579d8c00f9405df9e1438e62739e83c07313609ee292891d68e92cb7887c6d486e6cc3ab9d08756c5107d3e628c5b6f6a9313eef1550c7c3ae70fef12bb58adcc315fa8ac39db6458ecd579e4584dee3aa998c1611945b5ad76f09a3cfbf30db583055b387ee413f302f3dd730dfd73bb85efa1a48af79e9254f7b76be79e9578a6dea3dee1a18f7a306460d5c460dcce79b3f050838e608c5de21a80d4a0c5b199cd867f472e8a29c81cf64c63f47c194b16fd1781ee0eb033d632d346ee465106edcecdf85e09740c7c2878707817880791e1d6e12cd3897cd266541e8c56a5ef26baa8b5ef4adb3fc52dabce8c643e3d28b7daacbc5407a041cb1cd0ffcc00fdc4697efe0989dfd7bdff5fcfff98875ff73ea6e07eb0ad1ab8e176ccad74adf757ae7f42b0014dfe215109f1fd7c2d2d84bac4b8acbe42cebbc6c674b1ad07eb6df66fa1ee9c1455fc83c3c746fdb9562c80f6fe598fb85d0c0affddaebfe21f57e5786e680694e3dc6f1f224073717002647d09c911fbacc19e744cd280e3a753a8c20d997313d793daf3d871df3fdf84f6c826566c78830ea1be7d7c1750a312a4e97518af6a5ae05449cf5571480faf4591a4911e912a4a4bb4b25a4430981a5bb61699614506901970ee9ee5ababb96ee66e9daa5eb3df7def33defdff09c99793ef3c31c4e63105c100b087aff26febf2d003865b1c4ab8d6a74d3af242905a9d48ef302773cb6850b7dfcdc591275a57665d7be9c3ad239aa1973327ef1db6fedbd2e2d52962c1ad7522b6a5dcf1cc45e2e52d78811a8b2e50a429222dc92dcb1a79b8c5ecddf9cdb1197ac15b7a75f74fd31e0f9591220b67e3557a61385ec00269ae86acef944cce7d58e07afc008b60f1383b77094a17d9dd86b3ed73050746a808e7edc78f237b4aff31fe5ab85a2ac5a2d4ccc5da2c918196b7e9c5b3a78d8f0f389cadda96b3c2a1ba9b41140fdddb986462d833b01d9e16d1ad6ef493a09f7d039acaf547d6c91c7f2c1486d7f3364ef2f6dbca881f746a8c8211629debbf7b4aff2ef58e38dd1bd64dc1325e3bdd6c137e63ce4dc1f3b02df3222b7ceee2bce9edb59ad570c6b30c3b99328e74b03b6a385f9862cef6345eeb2223da4f72c899c19e7d53b72ae0ebb91f3a8dc8b423f08f4e081f3379c04e197c5d297f109432cd2cd1d48d6ac810741de0228667a6cbb605adb1fd967fd985d82fe7fe3bf6bd56188494482161faab176278592d5b87650595e6a04ea34b309b6fa8567a1caadbbf607d5e7f8b4152138cd32b186fabbc7ef94a106aed068375f9502481ca9a1ee37b5cac4302158c51fc51847fc8c934daca411913d4c193fcb65d5a642812ecfb41aa3eb6a63c84c01d357a54ce24f5b6fd0fa06551b03d21621dc5baffada417140065d7a23c49b651d04ac5e027070e9a24967bd38b73aeb7be09b852cf08aaba8dbecc67c829e5e0f5fb2bffbf9ae9fc7ce64a6d372ae7379fdf9da409fef82ce62d4cc0557c0abd38c420eabdc0484c5a5b4b03bf0373042e1726564a07d60d73c49bef69d2cc3eb97c4d784c3166e676bfed786983d00c5331a7319f714b9d078d2a573b8d33a6e6ec15dbd87f5c4c9a6a334bba2b497d3d171310e6bc63a515ed68a58f3cd2f4c18b700dd931215cac1600f2dfabeb4cbcd2a7717b03dd6ebd5f387a24bf1cc97c7330ad9fb4f938f93b952e9a5e3f34aebe219aacf4c5942df517101327f9d41ea01804820db797a76e0aa23a9b5447f68e5f5978b37fe7f1cdeca1cda7721f472dbff4a32c71661261f5e8e2eceed9d017e6725b9524ba227f205400a0d91437a2744673f33da863fd0abe8502d504cb96f1d4e717ddffd4acd2c398f3da5c491531556930611d68a7e5df499bcac8d0825adb4519cd659ab8171feb303415834f30a69983c054ccb3c256f4522793b30392acf59a9a07798a165c92e73dae9d0aa93f462809e357bde9e2edfa5b673187d3620ff363a363afa2c4bbdf10229c03e9bf958a1e3ad1f0c2a6c99f13d986926ca633c3dc654e6ada88470291a733642a5767795da2a1462a10a20bc5c4cae5a4c9405e9e1385706733d8bf920480e2c3e2198532f9d0f4b128b1feb23024ed030398392f28e70092f2966ba8f5d11ae9b993414b117d946d71dff64259db1f629e762e9d09d5ee7979ac6039edd20e5df1488bfc953e64618d85d36338d7a3a12a8ac1661759091edbafd1592e496e217804b8b84bb9f3f0be3dde2d04a676dfd7c129438a834d48caec62a0d26985bcca2f09d15be61e65ecb507bfea0b1b360165ed016834e66a80d24f58feeac833bd4fa752d8a46b004213d2bd4d5cee163fb9ba525baf62df093ae06705ec7b5c0b97d25f8643d459dec662347ea2f1b6b88fa0022242f446d5c0ecb4bc179c037e2e65ffc30aff48e83ffc047a54f361c301d91397b2645ace51ade8928cb76895735ede7b59a0d5dc7d94f35427f75713f68b989575e690e3b2b15266a2076c4cbd3d9d4e497de300819ba076ce7e256d24eb3c8fa1c75a07ac8d8961e7fb2fd4cca418b67d8e8cc8478d00fbf402a5151e6ad0e0d276432372d7a1cee726482fc8f9e7635d7b29628547c3697c615a27f1f59e8b0532d7d403c088299c41266b6c9e5f1a3289ba228bd9797d72e2530b33b36e8532b83e73474b383454a99c4b4ca87b768bc3618270095428b10b27f322d179286877776c5a1abe98464aaccdcd6244f47a33481b5469d06b4133ee97b62e361ebae792ccc5f08511311de535e86019a2e407a706928480ac7dd4174d65297fb9e388f71b498ab67e0a7ce0c96f4b602d3f99ffde8ffc092719d685a1b1dcb9497a96f9369dadac4bb97fbb3e7d31e59b16ca9225770e057d0b5c9c36217d47fe4ad490cddcbc4f422b2309d8bde7bb283cc390f252c2a05d802058c35af6f49aa970101ce5093a78f9b4a57ad464563e68bbbb39da5c0ab12aac02bddc02b38d9f481322a8c064ed36320082677762f1f324ac23a7cf54f52d0d284b1754e943163598dae41cc8b29d6671037e7bd0e3da429ec55ae996a9430b6beb1d1104c6b4720e90ff081b652a8dbc2e583ca87a4600b899d61f105ae6050a7a7986cb3dfa122ec394f742568b249e5972d5fabedf95a9f1415df6e8113c19a12ac1e663f6ea1b4e8d247aa48779fd0e09670d2594c668b03d3ff754caf584956056da30c41128c3eb4ebfafcff58d26dc34c8604e066493fe744fdc6d322008db371c736a1d1a151857e53b53535314726aaf86219eb9a8092557db6933a2dc93350b73b4933fbb6685a1d908e22f840cfd3952d816c28731d47f59d56524f7cb9f529dabe35ffa828c9691d77941ada64cae9bf95da9403d4c2a3f19a918d64c059c2553c2396a42cb49be6f83ec86b46468cfea1d7c8abed5cdc27182748eae921a837cdfc4c48e8748007fc74e33f71d3fa85a63cd475fa71bce26c360b0976f6c7cb0f96d235ea748a719f5d552f4cab40711f86aca18ae8656a6e109549993edf6f7c8943fe486f7c7ba2d8a645b6445beffef0112af2db1205c9bdbeb4e48f8b766fecd94bca635dd1e8f71626cbffc0c19f2c70078dcabfa99b7958d9b71ffeed406282953eac2740ac832fbb3cf79e8860a3a0331bbfab009dda4034aaa13b8b6f478d567c0165ed359b81b7c5509f794aca30bb3c176ba7908ad9559daa6b2fa79aae621e99c5c20492f635ccbe90af3e8cabb6c49c9c71ed9afcdf6f52eb01f2bb98e8a1b51b94c1916492b706b4418ed74374235ee56eabe38604c99a6e2de54b02b309527a9b966c218a58077bf8344aba3db79457f8e2fcfd0af85194defb94282bcfe698a50ae27f7026af582da291780178c01d4e6317f71958dbda251f34febcfc2aaa7c117b8d66f4c7190091d55f1c70737abaf622262e1cf80290be2b3908390428b20c53723ba01c88279dfd21b36669594ee84b198a0122efbb59e54d7866e5b8ef453b9edc2a2ac5c5a9cf09102ac8e04ab4eb6f0915abe4b448f858c1f48dcbe8d9260f58138f74d5926baff0eee0cb8b915f0741ff0424a08fa67241279086c83303d842e9ef2249e53b13e13e2bea253d2a16254d773ed92997fd39481cee561ade161e8002a3337c7a5a9010a988bfff3e89a26dd49e21fb7022c1d96234f3c1a61cb0fcfafb996bf09e043e7e1cfe5a1f81c0f7a99a44fc51799678cd5cff7f80358c388cf19073b7c3c1f711eeebda7e0b8c2af201be038cd2f8b8aebb69916521e743356fe3a725cf642f6bd03f6b4bef4e8bfccce3506bacfeb6c469a34d9536573e534e60a202a6795476787395a9407699d070b262d31adabbded3e5ea5bd31ca00fc5db6edffdfa89288ee061fcc75b6043a3d744850fec6df4a21d77ec5feda1d61ced21e4ac0b6769236f1425b9a00b85c0b9f3cb49c713a67c8671f9d4c0eb84c4d0045d0a462d7c5f8276021f921bcd71b3edbec10aefc85007d4a02cc129616057d0157fede2d67bc2cbbf1561fff49cb036d22532360c12fc9b6cda6b7d9c5a0271fe0d5e64cfdf4fc19337da0926998a834fc35771e81ca544cb633f357dde26ca569e97f5c1fabb471386221328a62e1640fee966b69d7e97776679577680a28fa5b25059fddef49ea88810f1855bf2c80872f4f0ee4933d66b02ea076861ef3c87f6d1c18be7c07742b2999568e84dad4db2ed5ef6ed37c2fa06a0fee883f65972c9c2d97cbf8fad48c7c529ffa6d2c03a657543e1e3a774e9c3818d22e4cb55a5cfd834bcf64099a22d8b7736038f48757041b52c7699629d8d6c838f6066616db8a7c7318f7e5635bb7439a48b80267e98d9ead322852e25674b14f7efe97f325fa795062153f7b7fc4e29dd2be542e5b4883823d5f06e217683ee7ee1ecc45413833867ed28aa226ecf3268da255b7f092db54601c77e0f0cdd9a89caf034dec4c35de61e04a4374ae93c37cad8efc74134d34dd93bebabda41958dd933523d47d232bd54c60920a6d421a05dbc139d0ecf8705bbbb24eaafff8baa5b6ac1f386c6b6134d3cd697e02eee35643b66456846eed7bd53513e4b190e1948570c3b0fad67d69878a73e41069a6301261f6146d7c53cc525105add78531d5c694a535489029d36a15ee06bc8c01dd9db6376d7d3931323fab1bb7f9fdb8da931270ba5454ec056fcc919e8c1bbf1e421c852c7ce076a42c41f1a9927d698e423a640031431d209b2ca611e7b92e400de0c492cf57b2a88cae7b9e0bbaab28197a2d74213e0539a078943469963caa50eb2bc9b7426bdf5b40e1cf47301d9b0f96c08edfca2337fdbb1cfcaf1b8de516001f7f8378aab9a0781e2d02b65c0e19c573db9cfb25878440500fe8f7f2ccc1295657d5cf00c26f3593297bcf0eecd8f54f306ebd48b544cad3ba96faad9d9f2017cbf9b82dc82a4c2f888d33df9660988e60914d38c1a19611861b5ce444d7b4923f64021d5069394ba971e4f5d1d7bf92bd4759f592c57605034eb76c327d161e7939a5101aa07121a504a39c7932d926fded0eb27fad54ee3f7f52ee5824067b14ad1035450cfab3c755dcf7249c0b23d7b2b875eb8ab5f73f7c82602f6f08afb535cc563e998930778e1fabfd23a2712e459cf78a17e8e5638e21c3419b2403089546aba09feec4f8061b6490df35b3384fa61b6fc094e5f2f10e5d63f1e937609b6c2c7ac492de4925a37fb1d390eeae86615b25e037d4699d43dfa87069b07403ffb32c408fec80c8e09ae0f25a7a3ba8c2c2b75ac52cee5eed91b173f7b145f6817176d17b6cf231629b0fc575f3a4db3020dbcbbc3cf337e25726626a648561c2b5365bffbf870aa09e9f67ec1266eda2a2ca4aa856c148f786015604f9e4b124dec52900d5010850e38bb00eeba12a11a2422d8b13e51880d5d8aed6afd83adef316dd7f595e0b57e87cb4ff4530a70e6f4375f0a9bcceb7904071b881c2a02337750644407583d25b0e9f1793026dd4e0f6c09643891063e1f8834875fd133bfe6f6b7f91b56dcc4e3f5fe13a3770103dfb69142939002a541a106db1b98bc995ca62290be5e90570f26ebece3f3f367324fcb35c5a64a5a922313b13a95e2b2c2ad4368cee746b1ef5697acdbbfe65bbc6d49d4ef77405c9f2f756a814229f3a720b2caf124f50feacdded7dfe8570bbbc41784fe9a1a1a6c86b115b564ddcf701a1e61e97a53884940f173ffb08b09d41b4f39525309db6dfc9eeb3458bb145f1f6a02c59a27844bc762d224529fbb85e174ffe2dbb86b9aee12743e490b17e16eaf8e8533a11d3e1a6974fce8ba772cdf8e58d19827f7e48a7117fa225654beb75a3171f409d87a4403723e3eaeaa987d8e95650d3a933ddefa4c987d50974b26b9766eed2e12f8a3c7b36500667e34a2455b43ce5fc66fe51bf1cff969edec692d4b9c50e686e48bf7a8396d6d94b3b3b3dfc85f97a72b3a7adac14c9030330df5359851fab285602e705b3860bb49a6cd67960ee07d174b4da6b3229b6118f8d8e12196069e1e1f579338dff717127567a6c3dd87df11156191b848dee5af6e0f4837dfa44cacf09c691b7fbdf7a6ae58d59e80c851a4bdb049faeb446fc6ac677e029e7d5eb0a63249cda167d1136020b7261a4c5eb77d17ce6fd292152568f5d90c11291e6083f5ea3842aa5e4f7b96f3720c3fb4e60f0e8f1a38e4d22a502be1a41491192cf14d286681fe4828fa6a533da5fc2eacccb68cc2b214d60dd2a164a078182d1a8d0afba51bd33adc206070f49170767a5cd0d4830c48eda126d625a82fe09672f9d15fea2413c9a431c117e5b139a7ab006eda6c05cd2b9d6903417c27669d4e14121852a39efaf0599219f87f3dd1fb27a537226d7d947a172376f148b21e2f364a03a8c7c03d2f7ab6fd1db3598aa101d0de4cfddbb7fae29d5e2079d093585a4f5019fba64b9c5a30ad7be8674e4afa00a920aef2a0bcb5dfc69ccd36ed68e928abb743423405a1901e4c682ebad6e9fdda599b87973d826921f5e59dbc3a53fc647affa86bd5d5985b2b2a4c73b7e38f059118fd042dfcd8e25fe475dc24e0e16adef8ec2081078942c3d476f36c3cdc1eea710343cc58bf7139df36b45ad7220a3e7255cf967a621e11d7135eeb21ca4b6ba42ba2c32a0ab47aee337eacbe4bc86af51d6a44c43cd87f42f33742f63a345ca072ed9da1e8d3966e42a3f7f057c4326f648da8b934c6c3c6dea645febd8cb300c5e950652bb42d48d0656c67dac674ebb2b37d3dcb26948ad9fda9e1b3f12ab759edd7549af1fa880c714a0a82a6e67be50ad448aedf3711d2876713e9c831a92aa586c1eb8302922ba13cf0dfa2ca7d1b5ed726623a79973880114f9b89ba3985b232d91436ea4992e4a86191e48a8b58baf3dfee233973c2cc78f32e277f3a57ca5a152d817da4e16d9c53cc788163412bf94da83cdc38dcb59e61d1258cecaea29ff7ab518ce94c6751704476f0c3694bac33443dc5bcd1352d1683df4ef4a3a815ce64d2c1db392c532c87da4177a06f92a684be7fea71e0303dd2ed1a073f8f6a9d8364ad9f8b4fa73a0e3ad4598f3999208dfc87a676d96d7823a5482f43688a761082e841810a1c651a212a768314a89d8fb75a440d0321b91e1f432973873549b2e7b13edb9d6d9bffe57d582eba28c414bb5061c59de3ab6c3915db933f236c5c71538e519e785b5689e631020d3e7850933e5546a3c94b83953ecb09af2267cb929198dc5f2ad0dce067c92a5681ed4580236d89a3d0d9d40819f019bac926bf875192c2d3ed9ac77c1be4e87828a1fdfd8ef8fadd6f47593f88f069a513feb9c714dce5df2376e74d758ace47af5658ad87c2caeb56196bb5925defb82e89200879e1f70cf75e9ae337df99c36268429c3ecb3150e1b8c75400f1eaab0e1949336738d7e9f3395a6587d01e513772869b693f2a619ccde96b69b1d933157ca34819660efbfe470d4b16ca682a18a6f8656815e086b17f60a338fc501101ac56d6ecfd2bc6fad49a781828749b5ecae6071093a0ac3e782653182a26a4a4789d3dafab409875ca0712f0a06a68ef1e9915f3dcf32efef1bf54db103817ef40884b4fd85c436852fa56092c2856c9e3a8b3c6941a085e855b4554a9805d3b43c33bb4779e242ee8b6e1780bdb4abc0ec7a787acf3c55514efc7ac4628b81e34ef990020ba53e381edae4d0132395c18e06ffc145643bbf46824d7c4acd8e4e235d61be07908990416e32d23f5c0c0f0bc6c7340c96549adb3f2b3e53d2ef743a6a4d7a9f8779a07947bd38dd6c65d4df99a2e86930a9487be4e5f38dd4d7ef0a9a6f02ca005e82111ebbc1db77a6aba1d9ecc669950c91ed32e39cb78d51cf45702a061ade65b11fd56a383eb80037f88f246ddea6e940941de6c27882213c81cfd12101038170f359d5e6a0c6ef54cff0cbc2652da5dc3d707c0bf990c5767a0ca03aacb3e9fe2aa69cb6beec4cfe7e5196ab56d3fc97d15bdfa88bb666eccfac68c450d18e4f6334fbb20898877ff378bb643199a44342861abe653b124addb88237f7078fd32764d76244a8d65983062c6b1ef0f10e84329fb854de16bc14a6201bde96c4ddfaa0487b779eb7e157e618b77fe26e392f3aade66bee4ecb0d8c1b8f204886c4e09ebf12cedd46c3ae3924fb2d94dc0889d7eb5bd3ed70e84f7dfaebd0413e560bd3df9acfaab2e951dc5d0a1e524251ecd3ade628b6b9e7df04e21854d0025e1adfa9f59e3d399e20dc2b8d344325da7e23a1ffb77419b1f6941078515ece2c91df3967800dfa972ccc677328dc7d8be53ce9b9c8574fd1fc0caa384038eb65f117d526ed5feb46ab1365b54917e773f8f0215b40ca8a5df3e0c11655b4bd5a5d86f3247a9d5d05eb1e2cd9fc26a03c835ce84bf803b98ed54fe0513c0bef5ff7a43971170f86cf0ec25332239fa78950cc6ce3e4f229955adfa8cd55d3cc4d679ffeb0d0bca61d6908727b906c4115944a7b1f62d09201328043946345ba04d8686f399a0b1c9ca03b79baf4f925c71eeeb88f87e8c810d27bcab3e6be770c42dfbec788e0659be12f13c2e8ad2897fcbf569b79e32db512086ebc7ec486a8c2aeacd92817d0124f9eff8f59548d20de50ed9249c39e83155ea53e609ffea925679628c9002f7d8d47b67a8136e24778962c66c392ad26adf276b65625ea69d6ae345cded03884cacb80705faf941f6deb3428e546e010b963f9dd97c1d8c4b075298ae47879d980705c7ff8fb86c29bda7d3d69024edd4f7a131266093d18dbfb21598dcfff0b4fc62c0e8582acf64910fbf9ed0a7b22c1c4447725b17fd4ff95c710c2377b3c559c018588501d156d98718f6f4abdcd15c2313991e826485cde78bc84f40529ab67da3c8871baff68d71f839ea626c6e03f705abefb1d5fbc39f8723dd1c5777297a74eeb781dc0390ca3798e6eadc18e984f69243b8d1c2ab20ed64fd8fee42e847194ee9b96b82ae9b4f50dda79962eecc334eb50e7f775ef03bfa3d72b2a32b0feea4457338e0888599e5bed0bb582d2e40afbc074ac04e87a830c46f3df5a80c20e09691a29b7c9abd70ab7dddb217c2b2e591c5627dbced5f3daa490ff18be72572394053902bfb1b3f9c9ce5e7f1fe73764f282045d94056c2435f39c92a553599f313311722bf752e539197eb3c5cd093c5edcb8305b7a36c6650aa45a302b6d29215467661b950a5c752bf4841feb89d56ea3f8576f429da4355e1b71fdfb3b32d1a6f4f2e5aed0e9a65292a3b51cfdd10df4993d61e2824cc9625ba4f815d41b1c55cd3653fb3f957956bfebe91c28611aa796902d3d5ece98dacdfc83ca5baf1791099640dfcb74e9713e8f43d60f1f454eb30e35d3719e7eb6d62b4cbeeb31e284a04d4e2bbc418b87e56b9961a14b9cef56bf1ebf8b929cdfef6a80388b5e044ff74cccc9efec0b4d37d28389cc2c701b8a514a9d41a1657bcc1f77e42f40c8464b0a22dc915348b3451c4e4916540bc01073e173cdafa22264b99e4394ea055bf284187b76aaa377b13ab2913e001930359e951bf984dfd98fa1509ce58db04f7a955a232e3c4db9fc4657b319573df027718a51143ea59febdf65d4ceba33341db2bd07b11b00f07c1b7b76b2084f3e60d4ac7f6402c1dcad1d348519bdac1808ba57d503ee90b03e47c1d0b539a73e1ef9ed88c58440ba65d853dd6b167db803b67267e93072576ef13cc6bc598a93be16d2bfd01edd99676838a1c27fa8d342018b057c720f46354207f90b18d5e1cc3cc02c0e31889b50a5b9b6beacd01c8e8668fbee89c7ea5b8138b8cb77cf3136ea94d62baaa5fdedf80da5ddee2a490886b54f76d5a62bbb2485ef3472b52ce71f89b4c865387c792aea76b2fa204c1a2680df7472643cb77a32051910fb2bea2d97310931f6d12070b75392ce0a75f55a0d54c8a8eef3d615865a5f5bae62d0af8122a9422ee7b48427e8255da587984bf88e26c55bf221c7bb053bfe7b6df0f654cba57f1bae46e6d4d54c6b7d53b68cf11abb6fc1afdf978d1b1a2e7b2c07a64092ed16de0fbc8853959e83dc9323051c5438fb816d21de12c4105401f7a5af7c11a0658fb30c2425d435cb40cb018c78a0f1c37b307f4936ea008fc25caaa3670e97d869a256868a6c9d501a1c928fb388a26f964200edf3b3b04224f4ea657bc57d3edb8d03139a942ae1258d4b9da0edba27b205fc6a86863d5f84f09ed4cf60f842a74c1d0ea9be13ed0599a68c040937e3bc16ad61ef9606b6bff1cd3eeed0aead74099a2001ebc23513db13dc32f942e9c20aef58e58c48d67e5f72dc3058c0f68b26d55098b153728ea6ee80be00ae1ed7a3d61c02586a2a09af5259a35c608fea09279455731db2f512b8d58bead45b58cb99cddfd4a93616ba63c2ae3848fa9df15470dfc5ee5293b97e3b716aaebd1be16f619a1c287e9b385af08c8702d25b0a05a7c8936b94f04707be8eb3ce9fe11c8f9c3d756766f90754cd50c589d95750ad87bc17ccdadefa0e605ef3c0b9f089d8c3ee3a59344c6ef17b48b401b7547f8eeeba7faaec30a5b4458da02271571fe73c6396aaaa7995a677372411ebb0d54caa633e1333f5aa7a0acf2779139918397a80c9b22bf0a6970fc897d6a2ecc503cffeabfff64d2807a1089612cdf9bd6de235645652d7c91b7add9b15385cc1ee1b51ccf54fe4a5118939f8c0b00d93a376d6e97e11829a378d2af7599ac2b7b9563ea809ac318af7a92ab9c8720a6927fc06d9b56fba39736767e020ef2874facdb912d0d0d314f8c57b5e07ba40ae47e4c78fdb875381f914be76eeb9bed7f20f58adc01432df0601a188e987ea88e3f66a58654144e58aecb221fd66edab70bd5322e796f55970d0c5e3062598c93d20330e6771512a5457468aae844840825ce51ec8b6ef2293af40484f4ae7633caee2f777605ad17c92bf0f0bdcb5d6a1fca0d3b03219137bf9ae35641ad14d978bdbbdfc9ac3c8b0c1743f4728a2d51f462a09df53671afabca585fb009b0f0f7319f7143f88102ca5d4df1ef30ec74c24b9a80413206877151fec1adc136dd35cfc36a069c3c8ba9714de5e58f2b7ce8450a89213fbfb25d8622697ae282324a5870da48b428d7978da343c603c895be26ccd6b2aab7b01b2df711083e6befe13dff3b5071a520e5179b1be6b2f25df419f288039ae796b6a4038a1efa3610be302cfa559f83237a9a39fb0fde86fdffe79dad48830be2473d0ec11e73fc40df0fd4b83edffadf39b4bb725fbc3cdf2764f930d31fcfaaee10e7a963ba1b732e8bee618db6704b4e7d12da08ba2f2942c9b828957e25d182452ebd3c1f6f8475e03b894f60d6938542b351e32715e78416be711e876974a5efaf9ff8d688b475a10e25cf7e4f608452035fdd83bbff4f9ebb66bd494a5a0c19271d638ada4b343d8de4f8e35070ce02c9ed98a7678395786d798f0c1424f9d24fa5fa6ddf7dc6efbeea091b46383dbce903fe3ebc4df772db5a34f62636c19b757dcb10782d4cddcdf45d0405469863607595166cd94fdecc201074f33837ffb464df7e767fa4de1aa17c972f70f8ab2295819c340bb3c92d43dc83913ec3dba7a9edbced79317494669ae6cd4a2430b4a5e7b394feb7e13b0b4047e2a74a70d5a840f7a174cd1f03f3a8b8755ea6873f199e43e69f92062ad5a535a3a411acaadd69e1f78d931cdfd9f1a7a22cad284664e4d36bf601d0c941a7f7fe9fff70dda57438a5d1507d4233c2a3bb84f9d8176507e47df13577fb2d1420fdf6d16cafa18e8d6bd57d33a0fed1eccda48ee7014194376c020de621455167bf9a1040aee855760cab4c02cfdfd4c3b2aae97231b95a300586b36e7b594ecd257ce5f12df1cc28d2f9a0604ac2d5095ef0160b05c9fadde24480e7fd4f058910eec42c83f6f7942efd5ac4a3e02194ee0dd2d87b02111ada814aa9bfe58d92eed7d91301b7cd1638111b2b20bade282aeadef087d8c047cc39f8b35ef80a417f2c314a2b3cd3cf49c2fd1e19eb866c97439b8687ed6df247b87d888ba4e8e11bf8982379593781d3cf4275d6097cf60165e03c65b78fe1509ddfcde1fcd249db12b53882055a65897d739f15d718783ea6d95395561ad827f290dce1a85c1e6fd53b0663453c6646afb6552eb69e4203d79dab38e8bce7da0a96eaf7ca20586e4f51b457157decee6764b63e6e976386b581683677d668b4b205a7579fdddac4267f830cfec5312de7e55ff81266e88aac91d145bd526adee157ab0a1bbcc83113e4569352203cd4f43c4cccacf3afd0cd3c785f0cb89b14b92f90649a88a59afda0551e501b6fbedab06655b81aa332d765f255bf2efa67dc22d4b1855a0a10377fad2b777e4e576126b0e8bd98f6f3f30dba0d351a1992dded43e9651ccdeaf7cc476c3ce1366f2c9417ba73d87e042aacde6684fd8bc10fe35f65a9a7cdad2c7be0f8670cadf56537a8b2d767c7638355b9654cbff654c947faf5fad32f8d13e108c08f0f348dff8101159483972861b9acf8d7bfde7d588beadb296ffd81d09e2e2fcf7a3a8f90269917c96cb93ac271be1fd7f9eb67ef6b916d9e0f118d9907ffa6c1b34d225c254eaf9c8bf11084f400a086be942b956848a9b1ddfba3d8bd917f873b61dcfe9bd313e962885751bb8ebc427a0dbec7aaef2e4446a23cb7155b55cc84e5c997a3fb9e5afc101dfd17e29c322f0e68fb099e1cea190b1d66d8282e7e4f56afaafd15acf99ae33b36df90ca422dcd9b2428c97d5ba327ac1284bd3d1c1cd4111ca1f61e95a157ef448c2b9faac8bee4530c1bc329a6b3e8f05bf7042b7dc474771b33c4ae3a5f7afb1ee5f49dd3d9f5b8dd3627422ac0159e127001ce0d717598ca570388b6165f3bae379b560cb61e115dd058786b5908b1d0705111da6e49e2ebcf56927b6aceae4a51fb129d8d0569b71e9a03e9759a1b19221e5088b0ac0b775807a9ba36fa94ad7cd12543917cd794123d2178875298cacc5ec9898cd6f7a188be6f706ac93c75ef5a08d1fc1e2443c7607fbaaf7775f3e40fed49ac09fc72b93b79526bbe4a7e8a02bbb93fa2df3871efbe2a777f90f595b20299c65580dfe4793d3410c02a2d23e4091222ecfecb6fd4339d6d376b0fe02024d9df08cb80df9a87f0c2b448c8f381f4d3f17a1254daeae153e03ea12a21a200213829f5329bbfe64249a221ad376d2a5aca54ccb18a75ef3d70b2727d674c92d48e06da0ecfdd976b5c0f423a0f2cb5fe98258eb18dd91bd735db9906dafe140ce5fd6a9118cf2457d03f343091cad97692f742dcb025dc057f43bd3fb25df2e689244d647a319330fbd35f97109fc3c254b827b2c0abdcf0198c350dae1033d935a21e3e9370df1bad34db41f496865e5d2d45ac814650ae01ef7a440f8c9ec5ce92036e05ce473a1e471a1d9c9d9f3d6490eb1b7a529c823e34eeb203b863822a7f044e130d3b93e8086b2192bbcd9a375b114a66a6763db0638c7f1833636a29e632e1002381f2cc6b5eba0fc18ab71fb2c3003e341b5e2ec7c9b92abac062cffbee4218d319be620815ea0fa497e5e06f3cdbef93cd7137d9a3c312378a4155f5cb7b4568cbc44e5b5f9275616582aa8e77bf5822887776940b92f4cd2d4ef937348591fcec13ac7ebc77f4db5babad7a2f6a6d7065967b3e2dee68aeb6fd5dad08b7115eb7039df5d4890995e4b880f6bbfd76013be7703afeef58077f8d4772d691862360ea0721bb8d77b61bc28188e809b495b638c0a9d1a83398c279fc25b2f269a9f269c6b41bfdc0fe0eded680c4deff669ceb3c840a672e4204613d0148a39631735afee6d2621e402372cec465a4c1a996b93834d9541d7fdb08ae9dc9d30fddae4753e856cd17fb1604a246ed8c2cf02df8dd3e8f3899cbff3a32e2dfcf61a7a2e07fc42d23bf4cab58a97d31c5eeff79f69d6e591cc85e7cc3b906b23c6e6966a041db45f7a0adc59385d681f32101ad31b0ea9a1788381ebc04b74474a912eebd66216958d91e2444e53be7f7346f74d3b9e643ea87f3056bffd3e49acfc0534f6eb0b1df897d0720e8699ea536fa658aea3424ddb7bc390aedc3e24e0d679c2310a574920fe20a98db62cf4efd6b85c79ad91a880aeecd8ba3f2c2e005639429190da228ea7aec7d5d8d21f8e82db93b9772b9e5428463d47a4ea1fb5b5ef4027321c006fc87c5fc9087c18479e4f5b025ddebcf14adbf95a50c1b8c9e915dfd32b8aaf5c61fef9268fe0dea04055e8671a1f6282d358ce8fa4d7f59aa43a6f191e746deedd4534740460b22a72dc3b48c1ccd9228150945acddb115e1dea7f75b45eaef27b0605dfb2d59dd5b76cb3ad72798a41ff55437255e6d5bc4cccaf49983663a8b0002e706cefedbd35ed654817e0d8db7f009dfebdaf5ba8ac4d32fc281fbaac2632031d9dbbdc934a767d20dfc37fc238d8ab17403b52da84701aeaa9468a11e5b0f326b4cde93bb4fb703d2e312c8dc96d448319e15f0008d7a97b7d8ecaea7a4cd55a453244a68aee8874f3378c4a2944afc1e76b6b775df9237624d4e9fa9f0fa86cc2a4e7851ee53865ce716a57a78b3c6621abbdbba14dcae693e0d676449a5a7c5d00c5703b3d20248a226342084d68626e5fe31eb59cbc967cf9b13996b772940e39bb21ba4dd8b6ba77f79d6389912dcf8b16ffb3737fbaa7f8099bf28326545483a62e9f9d5e465f56e5bc511ff9a1b373061d13b1aebbb50556a695acd6588f2eeeeb1bc821b4a719030d02c983d6cc780d5b39c8d76e793b3e7eff7393c9917f2caaf734638c4bacab4b5e8f1da4699a45fef2f3e1066f4a80c1c178b3cb1a59477fd0e4dc7ff013872c139d57666c497cb7c8ecf1a08265d1e11801eb294ba34bb7d978b89fe3c3dc15a70396d6fc3f04e4c83f8207aa8b0f9e656d6ce67db0a91feb0f96fd31f04018e6ecb5f88c6fb229b766f707f1ce180a5ee092a90d108b0e3d73882ab3019df375e63674b53d0bd07514f9799ce505867589016f79149db84aa71e02f104d913853b73ccf02dd7901bf0b2d3adc7bc2a84f9416741d6530a6ab190aa981424cb127c7a48781fa05500942b238e320c92908304a3d0aaf1c48615a4fc2df2f55248159b117abf3b732c6ae9747651d9aac16a84a803650e9377243334030237bea3b4e483de1fb0b299f7e50e819b186f48803d6472d8fe4049f3ee9ccb5ac3f39d15a8e765c2d541f01fbfd5868913f7a3aa1b437c0768299083ef1b1a69715c1f343dd8e7b74e106e38137a3cea8d8018cdcfa52df8318de97b3cfe02e64899be6ce334873696405e07c286d3251ede498f753727b879bdd6394f6b1bcea33b14fbd17bb6e41d0fb09bc3c76c6472731477b0ac7c1b150d52ac562ce4cff4b317235f1f260b7e5bde419916e885a8a657b377d6243495940f844d4398326135be99a19d35a965027ee1871b6b2fc4fbd3bb4999c037393579cba68c47b59dedf4f7073b97bfdc1330c3d94f285be648c68e9a1e372bb777fa7e602e2c66f7fb180fdbcb074effb179fb8a33f4c9329981f39e590b1e866820a8a394fb2aecb974d7d2aaee2cc5dcbbddc8ed78b8247f283a5444071152b6f35b26a974654695a69ce8b9497bba017a67a3ec0b0bd79ac7fb128e0a29cc8017daaa75b87860b46f272e7c8cd6ee8dd429384e377b163ca7095b4bf676f993e99ebce0df2150b68f26aef4c145055f6b1dc34be97c4cb765fb9bff88b8ec624c39ddd2e56c0965a36a13a401e5dda9a68473b7742a48736973524783bf786454d25c2e4c91f9f3c04647b0f82e48c222e62eec8e651361e3547991f94359e9befcc9eaf3ad7de0092cc14f9327439c97797ec83a6c14695dd23410b7b6f6f86f109b345b3bf8bc1db885a6b426b17d1415aa4289c111e38de3d2a3ef8b456ddfafecdaf5935873c0f95f42c261ed82779ab2cee950fabec0c6c964b03aaab9e9f5afd972c14c2d39a664b87559a6b81b7efd909db6b884ca8527e27e4bf718c95fe997ea975952c4b1afda585fac5667da639f0922177c9b2f5a388858e14c8b69e4f3bfd84e0fe81cdaaf0c5c15c1444a6ee3b29da578fa57796154b1bb746aa2412e9ae8435eb928ea7e8a576bf048210c0624bd00e5b11368492c4e67b45c19a680f0293056d2ab417d805d5fe21ff4683c6a8f58c695715eccd445869582dcc8ed154d8e13453eecb2a1f3b4808c5a60d190a084d98efc77a0177f32e98f44f0d8bfdfdec293ccf02aacac7d90598ec796d31b23df4fdb4fd7ca63f79f6df551bc6a935ef7d718953e8998e56773e1eeb480b0ddf34680a5196f159a7ab18cf37061b5f98d21b851c55bea0902263690f58e81f48ae78dedf1b7adc0cc4d03c1865dadc7e357f1e549082bdcd906189885090bae49fdcf108218aa27f7291945c5899ef78be52376d1e959e90bb53357452873348e840ec5af420f49a7107f9231887576cc9ef4ffc4f7d65dd8822aa2636b96aa00d0fe98e88bfdae63093ca1b3b9f9b1ff31f38272da9b3cb92fc42f0ba1c799c97de75c4979416e4271746d5f1399e72c78bdd6ab0d704ad6c9279f7280f7a7f5a11aafe1694827fab6cc2ce510ce52bd27482453b125b43624621eb7c03026cbf8ddb7b59752bff8832aba5086d43be44a6122f6dd5c41f4b2b2eb767dd3d9c692e99be03825e5deb8bb7eec3dcb4392cd44d4ae029febd4500d0b2605d4408089e671d0da23c98deea01524afebe76443f3c794fe9958bc92587f614bf2c76c2827ef06303b41e608a9a8516cb4da7336457aa0b1691b10cc04d735fdefd14c88ad246080477949c2cc5b6edc79081d1e6e0647a09b77b7a85de09c336b6e98231ab4e4953b018d3c43f5f625228c5614bed7a12e58b1617607fdec3f7731fe8c10db81af330c3f72efe9069e6b92fce50db2bae106d20fa8124759ff9fcc3f3dd12eea49f114d591a10eb38c87b9f964266fd60d54bdd779d70a4ff7693d22503516bad1e283872f384d179ee4e8a68bbced72032e312b979c43a47ed6e0b16f20653aaa6e4190e14a0d9b469b459add6bcbedb92249c4cb4f17ee34443cc3c282336240bfca2eccee08e13c2a6f82d48a73faaec99640fd3f61dc1cf9bb109f99237ebb0db025b32e5fc4249ad1d0c0d9c34bc652ec546a68bc3c8549ca169b92cbe4e0f31a74f89ad76c22b7989afb97ecfbede1da2576bdfcef27f2e5159fa6020c684bc52a6d6e7733c9e5c9857b34e808cc3a9ea2b2a1dec65b97d7dd7366f75c674e85041876510f94dc975e5dcdf1e1e105cf0a0626aae98bcafc5ff5a0e26bb6c429253cb03ee3f9437bb9369f7b8ff462b1f8ead11d0e8cc2b805e6531a7bfaa9c975d676b6b44b03b825b08cb28758d795b8173a50f1649de0b99746e75a3648b0ddd1bc9e66508bf78852dacdc8315ed69c49d11db7cc5db676ac91b48497b7b1a765c7920095f180d9980d0af9690baba99684010cc9c99e237613cadaf97b731608b1bdbf283f6da40a92026a0cfb4295d4f4f51f0bb2621a9b2e1629e1e24d1649a3472987017cfa7dfdbb6b3796fe5c7335b4578e5ee6f5f529407f5cb8b2cf3ddd5bdd4440eaa0ab9ca1e4f73cec90adcf923110c1225465e69d752dbe792c4af5c3aca6c89587cdc77bbbf592d4834f207219c8cb8ae3becf59db60cc444c1d029a4ad70790d09bbb8bf06b31ecb0ba62ccfa7577e03e454351c0cda0ca2d130a77fc5859658c9d36151cae75c6da7d7145b12f87a73da3ec3ff92ed3836ac9356543e8bbf9f4d645fdc69a3c275d4d52cc07734131795ca3151682b1777ce6f0ffc38fe70474389be45593feaa22f8f7e004f39ecdef0968a5b73346ef4c86579ca1e5688bd97cd8812cdc5e42a01007c8d19ec148fb1286a5271d53c366f986527ee7fcbfbcbb8a5492e82e36a5a09bff678077e01817f2b63c1c1dc139ab85e6b3e7069f484405d0713094d4a813a2276bda54994d9effb497f1b68e512c2f5d01643ddc504c7c961dafe9ae77e0554066871eda8f0ea9f19b65567f4e0b824850ef6b8004f670c18a1718af5a0783f772566b8899fd1a5188f31b730f8dd73cf7eacd21af8a6edb01f8b56e4198fe7707ecb2880d4bdece5fdfc6075336bea11af0931a157dbf9d0155c6ed8c33a406a85226e3f4c297cfebd4dfd86f79f5e8c86bf91dfd9b9b1572b7dff9ce59fd5a7f11d0ee1eefcd1d691016e4439206ce26474360c46d8986783dabaa74b26a7a03a142d2e9a6b9f96b47f810a778f24c9f4b807920e3f8099ccde38465c44890f8e347df3d1becff0d4f2db5f5978eff3de725b3502ac440df42951fb4010b85d4ff06c3fdefa781dd6a64ea9c250b7146de2618591308435f4c89bb766bde5df55cc25c58fa7185104a43de18e9aebe93d95bbe9091897cb4acf1fefd9cbe5a7ff1edb50b9dce10a1d911d83ef538fd2bacbe34751eacd771291509ded3e59c236dd227bbc0bc5d7db0935c0dc4658fb19f0a209d9335f97c0011a2f9eb796db59171f671b6d487c70f01f42700c8e4d9da06c5613fb3a4764e67342c94027ca2392f3d341ac60eeef7dd9a6653bb3c95ba150732fa9976691c9451e041341f4a9dc9963d7f9f13d2e4d0a4dd119a260db07c6769608595d3530eba3bdf829bd90f1d85136ea3e40935c99fbf439efe812517d9679dede38ee31716f7a420e2f389b1f06dfac7a6da7f067819971c12f1415add263df927cf69a03a00af3471af4637da4e4ac3c0ef3a91a532313b8dc41f1f65c64a5027bb20394ab39bc8e16cd51ff6c2d1b61833c6a0d44e2b7baaee72c9b525d92c25b44fdb513f5dc6ddf2293ba9ae2f1cc53cd5a94166ae80f2c14e47d9539eabb55be9b64f0f6814b331541264b9e9fd30982c7adaddeea2411a1e727e0ff7aaa73fcf38391e6fdbbc89a052ae8bc916f21c122743f1b222aa810c745972661ebd0ba93fe71fb25e4976407f6dd254e8e5b84e96d8d8d84ba834bc1197471398d59d957162a5d8843db5012292056094db0c3863d28e7c24069c2752d7b747d3cbe33b15b9a740f8f710293f0c2163adf992b94db2e46ac7482c9888bab610eaa3dfc699aec585a644eb8cec5f5ebdcf9204066fe4d291e4bbf6c233173119568b4de71867cccafbfbc1aeca53aa8f9399db3c0dff3c7d18daf60d1d5d8db055451e37203433d32be81c1c882b2f817d6f7a797f47d3e078c8aba58596793809bd08b283a7971f4fbac3329ff77870f57c62f57cb05d37d59661597dac18d1352ee7b33dacd45c9c35a1b75dbc3413332c641f4f43c280998478b7b813816906cadec85606bb4e6b0e466e6241a1e5c7c6d333e5574df7736467b860f39af5d84d56be7c572224705d5972e77524c45a38ba742406f130d291ddb08a88bb1a02b062469b33ad8bf04300ba3c9e19543250f04ca9b37b1c30c647729dd305a46916bbdc02505fd4058e1a992a9edab5da652caeca80a0384328b8bf6f1df5609b37dcb3ae661e703d24623c525d9b9cada8fa3caaeb6700a1774da9aa1af64e5a4d9fe38b09458da20a92a2b04673bdf6f3223785f68caa15912a3a22e6e44c46eb397f745b2b89dc98cccbc4c472ddda9dfa24b05d487a6eae6f2ea494ca4b135b068d0fb8699fcec8a427a78f3b4e52c60d4dff9069f7c6a19d14a6669e35507d70f7fbd9da0d043f92eca3a7ff8b63fbce6a2ba698e6c03f0469e439eac3609193e120766e4ca273c6fb270afaef7900d0ac073be6fded334c63aa414923ca0adacf59e76ee4ae352ec7846a34b7e78de77cff21b88ff8e75ab24e62c3e79fa805b5f3e8d7c0ab3a90cab116eddc46fe49bb82b45499cc8960e44a5bf0115a8fb6b7291f0216198bdaf10bea391711b827b4f35b8d9af7071153f7debd3620d328eb4b216cb8e37b3f935c09f7177483c0cdb51e072ddff2020f507df9bd9c0d9bb9d6f58d0d14f3b3583d3a96e8f7d2ecaa58571f3d624bbac53e2758de1bb92d67bd212c0816f0bc6902a89a187b959337d8ae4ef62b7dbe352f3041f5b16bfe5e2b574e8bf95af2b4cf77944c6c2b0d65f43e7df548749fe8df635527adf2b37cd95a9cd9a4b6ad6e1374c8871bad75e1034bcfcf06c55b84484b16d2fc0be71c523a6580c5a0c76f8742df5f3b0c07749530d2591893d85914740d0487de8f6573241d7cd8d459e704db45c4c2e4d341a8116ba1aec19d5237dbabd2cee0af84acd3b11e155c07d5566a6f59d26be7fefcf81a304e3cd1ba62b4523c628d39efdde639bf09fbd0503d87628820ea9850b4ad7dddec58276e07d4608eb8cd5cb8e43f56d57240084834f9ec27feaa34b6ed8fe8201cb176e53f5799d178812774ac90d1e8e6c35bfedb6c9463b8b3c715c151b232be88374ccf99db5ddf022ee9a2bcd4a8602db5ac45f126aa387b49bc5684f98b6befd13d6df303ad70c62c3aea731647f47ffca8d43dd2d10ca540f90af9c007446c31735279439e7f3f9da6236b36b2c844f0d0b43546180b361849acce6a3a1addc1d2017888c9fbe26f67e03fcd96811a7e4c7814b91866d7f2268bbbc62c1a6e8c93c2de4e6c87d955616cc1cd65bbadac675a468a055a6f1a7215a4977da96c48c4ad7a372ceb6d29fbd92b92bc533ccd0f6b1f8cdae3c94edf2d5a441ea3da7b6f45f7d42470d4beaae581ea736928c17e80a1c578cb27747760b00cf5911144bdf33aba00757b72d48ceede5e61682b9a77127f3c51d4a46b061b3a37d3d1892f264d6746cf2f8d49cf8d8a9e23f1be034553764ca8063e5303cd98701e7dbdb47d7b3caf97d0c38d84e99e5d44d957037961ecce733150944e293e7ea9a55a9cda46b447fe278fd9a642e4a435f0721a8726cf66de4dfece302d1c0860e6f7fe44f19349fc2739b2744c6eac15ebb384fef0672d18c37cb9d5ce41a414e13a539b68ba470e6cfeb2516702b4c878c2742a4b88de29c72e89fde5636991bc4fa6ac62eb3eba3cdbf527c295fbf2f5f16e1cec2c3042f99c57ed31362f61e83fbdfb96392c3c5369c3c7cbeb8aa627fc0980c99267189b62c3f493f064aa2bacb94d4fd71b978f70e625ec5e0fe0f378fcc643cba5a17e5aa37014f13ce89725cff6570f703be2bf6619fc62e03cb7ced22ea24d05930f6ebe5f688bac27b561fa6617b6ab69ddd8f2a18bf55c266d70f1593d1e73f7a087472d7d772c61fa7dab06ba43eccb5abd8b3197d56f8bb48bbf3ad36eecf87115b0dc960add78769467adf8d1372681d625161404507f62ea1e2c2b9d243429580b82f4f0329006ece2559e11762855e6e9769d7e75d528a16125b3df90f3e2879b8931020a89dc7f7afd564051a3d187bf26d3ed96a9bb6d12d85e446b6d25e93be9d09faa1f2b7a8784d344235341a0daf2a2e2fac63552d87908b237563e93bf275e6928937df0e887e105ae223d2d2195b5a8ec6c55f186891597def6946cc8d05a7a2f43c5d8537459dc3951185b2ecb97a6b97de3b02a0c9de56c1348868ce9e2cdec084bad78dda7d0e9b2eabcb80f93ae7caeefb570ed830dc5e67752baafaef5dfeedeb695c8a67da4087929c096538d34210c27549f96017b759d2a1fd5e8cd5583df0ddb25fb147c781aa333409812c0eaa5b6e16b8ad9adc6147c950c78ccadad76627f56109cd293652004c80f3b6c45e2b6261ff466d33718bc666c6f86391ccbd95b4ee7670879b7db5c437fd32ee1c724b4dabeb5ffd218d48ac4f99ecbbd0e505e2bb6e852393bebf2d960f4c03e877885f1992ccf0d1cae7203f3ece5fa8786424b9a9b6fb217952f7df011f82641c939eff24cae5e8476c5679e5335b13a569c56331566f06b10d7ffb0296f7bfacb9f1dfec18c3445495627fa8c4dc36a217ea64efe2968542f4530ecd184b2f4bb81dff4db62bd5c65f4d9e59fa6440dcb4afa756885837e2aa26229d443fad93e22650543bf0bc05870c30a8bf319a0d1323f037fd7aed5a12f9bd492227e67e375e508942d0c542c085bc8a16f51ac16686dd4d98e02ad59e9b1c386fe4859167de7db9fbb15cfbf8cff0e4cf3b73f4488af7159a13f0e62bba83c50ffc854c6c2db3788dfd157f394317ef0ea912b427fd253333137e730539a9c40173fc60aaf8f0826d2b69f1f488c5e07bfdd683c6152d04ac45d8287b2dd6e78df86905117e59e86566fbc754af6c0a3583c595955ba54c12c9a4a38d2fa3dfac96861f8746bf8c6b178a6d6f1b8793f5f637af1706ff72c328a179ef7640e48822901d2bd207881e05e3542187b7a143e2b48083d93c2961cad7c5054416c9a5dff3f23eb0c7f7327ceecb5ae9ccfb614b28d877a41ab04429afd4367e870b1af7a7167588457daa24d591b7fbc28f6435a0e5b310d5d18bb3a56fe87cf711b510fe95d107aa21286aa257b912c32fd81691c8f060f4d1ae804390b534621ccb71e76bbb496b449da8c5a557f3ec5c1d1be5bfe086d2b05e09cc396ced2fa5d51cc24293bb937959d3daa03ddd6bab48dd51d2da584f8a88139a5a821d84e5d495cf3e2c3b2175ef636b0787ddb30b1eb59ac7c11aa29dae105a548e45271f7b1f3abf38c86bb71184c9f536c8037af504085690d8ae103ff32588dd0f24cd05a9847e95afa5a89371e7ab56e2d5a064ee6a0461b49af7a1ef58062e1afc94e54c45bda1ac2752137aee0341fddca0c23dc1ef7f8a044ee6c614724c03fd834974a7ff78d3e419f0a01f144408db150cb858da98ec9b36ca493e8539ac2eaf5d37bc1117176f04b8d14e34fb53cd77d2e54a36ba8f4ba51c6ded05d587effe700e2ee52f10dd17bafaa27dcd00f56d733f50e456fafc765e403ddf9002a7b8caea1fd605c55b8e2645541bb9e6e6e41d06c2cce8869dac79eb4b549ab00d8cd3a38fc45f9768fbc8be01337f146ba1e16b67f9604aae5ecb664ac5f7362085966e0cd7af973f0fd4851cfd94d1c25ac023164ceae3d2433b210cbc9a0fe85273725afa554f456d4b8e57a23ab9c413e236f439f9d7571dd71dec2dc49596cd257af775218ddb92bf8fc132c9471f0d61cbe2814bd68a6b9f4ba280fd63f3fd9591c2ccc42f79f5311501e8f54f477716cf95a2ce6aee62e9c2e8d93758e178fef2342fde3f076f3dcf2d3565a123cde9ce7264ec625964ab35ff118fb74d17a3c47f7f07da10ccb9aeeb5055e91f1f62d1478cbc567114b39e894d3692fb8bf1681fc8570a42f4caf80c785b9d9b03d8ff6f0bc0c29ebfe522260742f2890ae14c1108c0cd995644039046aad74a6c1b77625bd6e4d115d909b296a2f8a4419cda7d9ee78ce94fe88d52ff1f003340ccbf756f7a9ff232ce4d573b63186a0030fe28cebd2f67a515a6ac6c3c8173eb564ed2270252bf8c139417a7ebfc0fa0e36210931adcc5abec098ed73b403380554009582eb775d1431ce81990d8219d7bae8c0ef23a8de80f3ada21536852a1b1e42b1b62eaaabdf43686b400d4847c02dcf0978c94c053792b5a6230db8726d3fa93573bdb9122b205c8e927e536c120b5db9263dcab9d2de521912fd9f2d08a5615bb4663bde48718e9a9aff5a3d593fb03a52833f24a6f7b64f864ae061935dc2a6b8d4b8995c3641f04dda1b21d4db6fdabb656bbaddf8a6a9c6a8a2427b71236bef155afac01feca7e0fb15750cbff4566c62df687867d1ef4253a0f50dff5b54f00be783d81635c6ba29b02a1f775d14d9ecf8b6e19322ef8012369babd343e860b6b6069942e4c36968e1a1835b0ae8187ea1d7da87cebf55f8be9bc383a9fff121f2fca7497008c4cac04a4e8ddc3aa0544c42ceabdd30eb28db12d03ee0753ccd3a83a4f54035fe60fef962f5845716d9acc74e3e790799adf2613d2cfabd2cc4d67b3bbdb5e22857268a83ae33a48eff9b577cd97311c350dbcfded6fbf9573e5099cd34f02533f8e33f689fc6ee3447c3cbf9b3d4f277c906be6742dfef7005e2eab97405cafa921ef6178aeb7749fae51e77908600c335e70c1605d1b96eb2daec74f8d5f39d61197ac22a51050729d85dee2bad6823b1546db4a68d1e73a4c21f988efed2ac05e752b6c0a4a95d510005f1d26e5b5697d5e93d6edf5098d325b1f8d0894a5b5b97563114eae6fa4c2637b45bba9575632c2229b724858a5f936849b2276e5a75f51d678217d78edbb9d526ecbab669bb6b3b43be28ddb7d538e4fd956b5d467dacaa5cad82515a41e1f24995bed52b8c7445da4a2b6572c34f6bb51c35fb56569c68dc95b29f343456fb6e60b85a5077df510b0b9d0274c73f1bc9b32936f2581f62e413ac7fad0af8b4a3d7ad1573a7bb862173eb20f57ada3dc5103a306460dac690090fe6cbc83afc6229dd056c5e869544de835c4c215348711635906be99270c4ccca1235adc570ab00272837434e81abb6ed8dd2f76f6a68b6fe406e064e6bce4c551792a40bff4a86f03d237ce6dec3aede57c6ba677902ef71ffc015f1f25e84de7b7ddbdeae68de1dad280535408b7f11ec3e3e9d9ed00f2c771b67c194f801ec7e9f65830dd313ddc9e7fe5771584a9033644322f9af3dc3ccf4f616dad7f2d891c0d345a0251a52d13dc4610892699a4974068a42d54480e79555d08aa96ca0c08f47281a0def1a034bc254b69c69267dc246965b492e4295f409e2a29914282ba9649938c573cfc1086990d05f2963cb8525eb24313945be48d30b2eb9b08c5276c2e7e98ad308cee8ab96e10229846524e36430a3701e88dfad9a5ed7028b0d86d2f72fcab3639fab47488acca7eb2cf5d4467adbeab2f5be531d2615084d463badf20d1569b5bf2ad368a48badd0fd8a2d269fa45437d417432bf77b298fd1ce7d47b427060a353bc1c0ffb3ce8d2752fba9e77e2f8be971e748b6977e6a1fbe1a2a1177ddd317121803e9c8baeccee9c1abde86ae3c187d1a3fee07536728c1a1835709935f0a217ddf16c4c04207de3849e3ced95e026860bc3a689d5b669f874b1cdb1ac1bb1be9296c72a44966b3c3bad06120397e5e16230cb500a9a20bc777b6f7232cb192c41facac0d9c5cd3eed85f55ace4d0bb8f3b2e9be25c83060fb1e35afaff232827435797587dffffddfcf141580ecaddc303e81b3e78900f35b39cdbe8c13ec4b04a082b180306f27dbb916142688f669102ae8ab8cd4348e9ccdf122fb819e9cb7520548b2970159f5419c243cbbf929cb18754ac25e806cdc9b595ae1490e9e93a6530965e18548495c5aa10f48a5c0b4f4914bc2eba56a8880f010238f5c2eaefc358449571116e9005eeb550aad0878352d9f1c4d07d2c264dd456979dd68a4885c73a4b1918268833558129d7a81db6176f1c423cbfb73eb0a4d3a6addd6839ed06ff265612091bd8d0108308f16a74de44bc8267d205ac7b0c9b5883ad216e86cc232441f36d6d24643ad599d2520bbb5a58a23c8ecbcf7ee71a17eeb5453392e4d4785e973fc4ed1d75f994d17afe001e047f11c504b81f15438d8d45ae77b291d3ad0257125173933c565501880ee3c74435fcde51c9e74da3804e9c3974525759acb703f04e9234057339f7b1881fae7aec351c2a88151039f8306f898d133b149afc6437642231857978656d38ab18f010b1030a931f34739c4dda8c6b686590309856514fa18398fd3b1c699122327653189d38d7f853b09c335347615c7882fc1b76b2a6e1c636e3ad6adcd4d5ff6566f1546ecc0bcf44e70076ba6df31ae99ded571c5eedff7bef7ddc2b1fc5240c7e339f6b7b124e863b951bc0de0fd388efee38181377b2e2d7881d2a9139caf0030a72000cc5c333c27627d4132902ae72231416083739e8f0193c89973def9d267ce53b4e219e93f913a6f73fe9b1501e411b74e6b9446f25c225e30fc740f9bef799e320b1b1d19ded40a00a5c8aba55e4f34267d922f4cd6074d2b2baceb752417e4d996cc164d23aa8d5228c79e581f0910a6205391355d2499c46da37df72ab4dc0adaf52eb1b91684573ae52a837d8aa9c1bca886f651899359ac3b7fd276f6aa2129cbbc23e9cb53060087a7645b49b4d4da657de65967f52a493257a09f04ccc84558c6296915ec79422b22a2b7013a28658820e567f5a964429f0e4aa32cc8a83bd48bc58788bf92c3f52b9c9f4cd34b361bc7aafd607ddd7b6e690f50c7832e88afb3a34a04e8dbfc74a01bd063965b343e04e8a62fc58b7ea1b5d13b7057d6182e5d0375265c3afd48396a60d4c0a881cba601a6bb7c3906ea2de08c9b85021abb0090f868883342c574c5b6b9f11f33a3b162e70026d808a8206e3a2080328d9164318f1a3d1201e9f22d36de3b5bccffcaca213504eb943bc5a5ad9b2e50c74066dd60f6cbe5181b48df75ca0b241397631c4e79316f0c578606defffef73f660b003e9f4e6fe53cfbd2c5dec6e380778fa5757f9813e8f11cf19bf738ff1a42229b732f4050780afc0d3ae2fc4c1e2712e79c8033271cc9cced0e979c0481b3f944e3a9959f7350398269690ad815c8373f1ff7919750e0b764794e57fd750e2fcb2ce8952ca3914c1dc86be0bc03bf00638b43ebb5651f6c4fed6968da95eb47409b8b4f395e378bc99ed71914752991cf591fd96cfd97b924a4825c87de5428a03cc4a5cbd0f47628c17a5565c0aebcc465ccc783d41969929d859b28682bc33c81b4f5db4efb936bbcd519c111d80824252a83faae5b851c25f26b6f7d25a3b71772f2ca3b2f27bca94b39ea438164241f99a135abe4e72688b29c0dd1e9aa4c1ee996e3976208b4e123e8e537f85aec6f701cdf59b97dbb1f9c9bbb0ed03ba57b5771597f49d47c74b59c836efa5201bab48c8387be2c6a5907e3e33417b57179c2e851bf3c7a1ca58c1a1835f02035c094905bb048af0612dcdc41420ce71e060efb256010086804eb05aa326a9a3f2ddbb4ac7bb775d0f2a737cb3d3b65645586649148405601985726199b77d033d588f3e123e7a6e703a42cc74858160d237dcd74f3fa5417f6e3dcf4a1921ea6f8873ef4a1135b8bc51f39b7b1f178aa781cf7617f1830772b68e2d6bdf9fc4bc151807166db7ada7862e8ed04040a6403b0017d0178307bfe85c43dc02dd3ab8a919cdce1591270e90956b49e9fc63d5b058af53e85f82d1edaa0d5ea7ce14b974214995a5f7691b9cfab6d5d09051e058e02bddc94122f3ed2a1b13d824aaf893aef738e93a75fb735124a65186865de1ab4b7845421a375daae68412a4beb89143ad313af8ce4b2d90045168a86de8bcd12dbd9e5a531eac05b13af45f52d37bab28cccfe4268da695103b7e5a10e29510a2c33d836e4984cdbcde358d6cbb7e426d3bd47a988d236e5e638db7e667378ec282ebd11e3df79e61ec75ac9c57a22216da213e983226d8ec73dfca6978d4bb5e1f366aa3f2db1589a9c3f8d5f399dbfdf7450c3fd4c5dfabfe9ddeba17da72db4ae83a19c0a1704e778cf7d50989744f5a0f7d3096101e8db8bdd63b8d0f5a25f0ca05bfff9be2e7ad83417e94790ae162e5f1881fae5d3e52869d4c0a8814bd48020163bf82a80d2136511806445160d6aec6359c20013cd5c0c3b0518dbd84fe93064c63b382123410fdd0c2152845f7444dc2f3396b76c7e6a6363f67f660df4b279942f231aaea5375d81a63568c3b9e91a49e7a60f5f20ed6ba6cb3386cba70180f8a378c27f2b87e87148fd5200cd6359c3fb712cf4fd4738ae8f05e43d7a172422a08bd75300c87424e74be3f9f3e4aa738abd91f22c07060584e665654fa4ceefb908d072be3799756e265660581056200a599c7cb5aa0939f079d609c1e43424c6f9586b80374029ca9316f9f1ca4b67f5cdad1a1941b6f46701a897df3e0024057f25dc7aaa6f61f3e633371f119422ebc8f94f59d8f4925bb7092f840822ee62dbde4ca45d641ba0f1b2b15ee5a42ce9e24a967db5883fe57689739a1c1efba8fe90e55305bb54f4968b1cd1876db19dfd22b65e8b936fdd240c94f7e93b5537d260cd4632e9f965fa483b8ea953f16998c412f2536183f62ac3e3906142aa469f27218a4db9dbaa2ebb246c933d24b8b10f9e7bc8b6ad36c92565730e4a9227011e1aea0f5df83e0ddd6f300ebe9192b7d72188700a0f0f7b533e50d49dea6d6e8ba357f339ec1ac90a2ef8145c41a88700f4c11cf4b34c71e106309e87c3a6b8c87731807ebe692eddabdeeb1ef79f9b0646a0feb9e96fe41e35306ae021686036dbf83180c7b3da44532460c8315e2bab87b1d2e025a319498d75cc387429d330cb12531ae6ee01cdcb614d5c4a9b35951f7080615c9cc698f16d6ce761c6c641bd1eb6a13d97b9eaeb73d30f506e6fdbfa78d3bb279dbdb366c670010df805d0ebaebbee314c337ac2e6c6c67f85c29f00b6b875b1317b2c78ea4b395cb7eeeeed9d98e2bdc5411cb094630b91d030de524f15cf13104ff0673f1f385f04b7f55485329193e74d035726058901f7ee951131dd73ed491322c09be550780ed28e80b178948dafca422fd2e306618313ba409be02cad6df8b4cbb5c9e948d542db028c6d96f5401600cb1326dbbafaf2ae75da15ebad26ba2f20dbcb8a3fd339a0f3dd8ede87b0c810d00a66a41ee7dba32c23954f59b5d28ae83732d287f03512e3a162079f4b2dda209a1e9d188fae24339ebd5ab62da47300c8ac3b82f4d1d2dc384190626551ae2eac4b1905a0698d792930d732b84d9be40940a16192a562b289d82069a33bea8a62b9b1a7ff9c8355a77420e9dc4b28933f43bf09f086aa579de3d468caab9f1ee4b8a529cbba6d575aa8c23fbdb7b1b80f79afa7f36fcfb8973a53cd4450b67c41a6b2f64d6de1336b094e6749c8078a8809d05dc1c5e60fc23e804e3e7abfe01c74591f2c409767f4a2ab8587278c40fde1d1eb2875d4c0a881f368809747bf73beb7f8364dda3268d5b4a1d96069347018530d71ec0ec5a16e5316f48e17ae28236e9986bf58ca70879e8c32deec4314396fe9f53a0086ae6520233632eba6933765da8b45ce79e95ea78b79d305e81dac37b1477af7ee77bffb469e3c3c891b223eec33f585cd27716c9fb839db781c10e9d1f3bdbd6302c5fa808ca052d0d7c05ca623786270fcdc0a284196f30059cf0f8f2b2539550442a439471012b058f8b3f12a21b49e1f04c80a3a35102a2fbf2aa024800c22411de7e1f226c0730f3925d5e292a2f8ba712820370fc02c3028ad1c29a1517a703dbfcbb12ca332ab3d75cecb415ba04b9f69ac97885969bdfd93820425fca533e4e4d26979529a5ff5c242340ca1f35a312290b572db6396f4a125653f9539635bedb1bf5c9321345e7de9e039e26d68f4033f19b9feaccc0e1aacd31d7a987a33c35fe6849b671b6c4f3580927a41373ab62d8a418b992684ec6a46a4a5ade93df5db0e0579f3204f6ef2c80f6fea261e9529cb636cb7ebb8a9e8341546fba9a828df6ed5c054f229b1abf29ae1bd8147c53c6f9aeabd85deffc529ce8d37711302389fbc8d38e34a24932430106d92e360930187bd9ef32c1b4b5c95c463def6ecda17442b3f2f884ad482e0dc68df1b3f08d01f20f77a7e0ff092e87660fff900bafc874d73e9e0bcefa51bc3e5d7c0e04cb9fcc24789a306460d8c1a186ae0ae3bef7c0eee9c5fc27d754ce3ae3d8ea1d3c491362998702eeb120c90abe1d5e00b39faa01523a971c4a00614c90b9f1eb850115748d1875023ba83b7f31ba9eb74bc146c168b59f74d59f76e966404a99f03a41f771a4ccd4d3f2f50773946a7bd1c6580ce5ae28f41a7b7a1bf5bc15fb703c16e45995fc13ce6273037f98b3c07a67cd447e0ecc1f408559cbd07d77f8052b0460aa4f7c84149a6473e7f1200828ab7ce0769046e960bcaa4169f5a8f71ff02dc5324304c2460cc5324c43086ce934af29e1d6a7838512dcf396b5ef84a8e1e600161971cd6b8a96982e7a3f2d89468a9d2486408432b5f6efb11d13218d5d59a7ad232e2d51f334b06fb005df2ed3f6d2ce948434769b1fc16cb633bf8d3e35dd37d223cf9d513b6b613e05a2fa2d6cdc1f246208daabadd460eb4d61b49b65941044170e6eaab1b9b9d3c6473fc72c36363246efa9628bce61313b05b8118da82baa1a9e3dc5ba03e1c3f02ee95a3ba90a316c2c4d9a4b4f43d32a30a7208ca4c1dc6adcf7eb786f6f2244baed17aca8134f9ac2fd2dd916756abc37ce9d1f1fd1cffdf05c0bf81a6f041a225062faf39507a93f167d73d856ee673a6b5b4d08179922432e77cb2057591d4128c9dbaf6f4232f88fa4e8de1c1ce4197c7174587fb214037bf4f6b19c1b9daf8fc04cfa7318c1a1835306ae061d7009ef4a763c1efc5c49d88a1a446e7116b6c63f4b0768273d3b183695133ce94854c028db9605c5b6988716e4ce605d90124d897516fbc45f2db00c0efd4dda471642ef3d23092d458175027cebce7337ef08357b10e5de9c51748e5595fe9e55af5a6bfe10d6fb8757373f3093c59781200e4718070de2f98dec6d38d27cd36a6270461eade35ab7d414ea09d17e53c72c47d99703a8388a487d11bac8d69cdc19e258379e5e41b0a8f03ec7322c097028fa94009f9d2b1115c158b02f9811005fc9e40822f83144237a7a2644fb6dece4e2f4d51722e0ad890dfe78e279fbcc8427ca64744aeed685cec0b309a969ffa2ce3d728b297be80be6da184f69a278d2ddcb371f2d78e9c921120284deb53fa200d9140fd1255754a87cc5c530a8e0c9ba2ae04f0eac83aad856d76e6cb088dbcfccc16a03bef7f292bf5586e7b23bcb6d018142dbfa96c45d7293245a4aa0cadc4ca4897db36fdec177e898ca4ca97cd162184b20ece434683dc7baea526652f8fafedaf7e90593ab31d82786994d7ea5cbe901a29d5d8f42975429bbb0ceacedd8375208e4d1f87a8e7832c85f3fff096ed3b5851e6f7663396551c06071dc2ae034f8b334609c1c9a877649c7c5eb1da36500eb5e97335b585d87ae03caa8f14b5828db335f79ceb2363547f1ad8f9d6d742377f04e85d3b57de3ea7ec95d7acb145a306460d5c4b1a00bc3e0aa3f1bbd8c0c7c6335e163ac63246576b17d450065cc39ba0d165945a1acccac4cc6268319252f750a04d532d4fe3a35080d8e591ff628cdaabe2c1a26c08d4295bbe44ca0a22bba6dbdcf423e14dd74376e38d373e1efd3c69ba39bd6d9335c539285f861e6e03fcde06303f16b0adc299c7e2146dca734324b0068507cb08aaa533e1b1eb807dca040a0f578178ca25f1f802dee5cfb1340fcfbb29c1bb40b66499e50d006c1e4c05b763ac4c6f0e7ac8b9625940e30a0e069701f6c2ce463a51678027f102c68885905a21705f678e2f0506be0ad2843e2956082097ba5cea4f8142cf6c235c59e628036e580324a198b3b6217acdf95fc0ded60b06233274e6a45664593fc2c8a816a92b6f3be4ed3510298e5467e7a42d700b5b5d43f6a9f1449fc84dfbc25bb25aad0aab2a2387f6773ef3533f11f98258d913920c7d2522dd6321dae7c0793ca9307d2f8d2b2995a313687a1f899b9f6b39a28ac6fee7d887a7745151a81d0fa2275b62280915abb645a23762fe21cb5afc4f886ce8ac828dcd89873ec70d0ae4d707abe8155f07dd58ecbd17fedfa33defe01ef03f00743fb544e04da4bb720a30e2e83a27b044e8c039d0a305c44350cbb534709e69779d28c57d43fb77b759bd45af79f7a09f05a0735e1d0acee5fb5c00bafca3175d2d7cfe43b7579fff9ac71a470d8c1a38321ac0d1fad36084c762b39bf914ac687cc928e41503e94630967d22aa480359404c7e5902e02c31ad610dea334a86ff8a26d2bd90e1b36032f99dd5a0672c84f157951995643271ca8bfb73d7e0dcf45ffee55fbe056fdf9f00b07d159ef027a13a8ecbd4f9e38f4671c7c80be009c8f3e944d4c66112b0ccd8a8d780583de01d66496499b3992bb65c3ed31c9effcf110007425c53a556e609376cceefa5d6fc02e404754efda02d4af514c95c6296b1700a8275841759d6ecfc76d3f1a6b3ef20acdf14163d8450f9677ea4703390555b10e207883c1f9c232f0097272723e02fe7ad199c7079c1d368f3eecb5ff16aa7203f73a891a1903c5590a6bd7caa76d24ebbc74d897a46540155abf4e4b51efe72de92f430cc5152caac5b19f63d273acc61498fc8231da846a6c72334ea4746b7ca553ea1f5dffd9efdf4cf329bde3dc7e15476959bb4dc8322784f5bd366f2e48d60da874c56e769f5d8c0e2f7fd0269ea9a354ee7a293ea330978a5271261b6ca28fcadddc9269764d151bff27a9b65b0bdfecb939b80e88c3ae4b1fdecc3eede63cf7fb2ed0b3c552b224a7fcc319fbc03f9efa2bdbfb7395fbc8bae9daeca39e2d33eaaf4fd6a3499cea7e7bca9ca418cf73c078706183a408f57bd98c8aa692d43baa2169c1b5bb4e5158deb3dcf47d9f292a8f7d0b3032fb1af03f4ee3d97bfc7fb1497f3ade4328274b5f585099ea7631835306a60d4c0c3a601a6bc7c3b96fda55610032b68c9c8a3916e4390461dabea0fd42325349695714facec3996c9a910bd44e36d812110a9f1696b8bd797d6e2699b6ebc0743f737a50c441f4c7bc198c7003a3f5d6ffaa5cd4dff03e6a64faee8b9e918d72f67d9cba700ea9e86e7fa8fa3d23f86a61fab02fdd35b1d0fb74a31cf74f2d4ab73ca052dec2976658cd27df1ba8e76682975559688885ce0384bfee94177aa8a3c1e570001f4f2c84f5cc7b9c75a12f22c97dc8c4c75212fa9f0991b4239c36399edafa30c30b43d9e4ff0252f64405a3052ce2b72cb9b4c9f088501f5309b92d772f3d9fb077f2b5ce693c15fe59794a2f7fc5276ff726e88220bf9c851660989d00042f36c57d5575d0a5064935a6c8720d1b46cd9a7c40690e687843ecd26bd145cf36f90275e7c65481aa9899828f06abbaab0faafee6c97d96c14155de69a6db408962d3709b649e00dd0cd8d54dac371b12e684a0c6d8c241b649b90eb6197b7a4db9ad01701f9de0978fc5b5b38ba4a864e1e85b0a5ce8c11d026cf7d788aca73490ecbd21eb349799cd4bb41154296bed8dea4aa731f410fff9e3ee12d9ffe1ee7e7fb2dbd78d8f586ea500ff88a37a0bc2797887e6f56f3d3b781d943a4add7dcc9e682f3ce34d9393639bb71f682de7369875f1235dd41f930de01ba79863e07ddf808ced5c2173ef4dbbf2f7c4bc6168c1a183570cd69e0ee3bee78cade62fe8331b16cca0636038951d5d86b480b24b1c7800b307cc4ac058d89d558936f8176988896b58438e73906968dd16679a12c9a48a0e200bac99b33e0b9c944d1302a30a15e22dd9e6c2fd73d5b79a7fa4a2f9dd6bd20bda7bfd02f92befad52f3b71faf40db7e3897b06aa792a08f62b81cb4f43b1791f4075087ea24f8055006df4a80e804094e7c332e4e995c40b9863a39e290a4d0119cb938c0c8164005b546925fcc3af7c3da76ac879e602280f8de0492028e617dccd74d37b23000d199057bda4a85738e5d961596b05742a3dd369acc3ba4281400ac4fd7ab83d5f0ac8951cfb31809d690359ecdb4d5f0779d6175af868a7b5f7107a73d21c6a2d854068a5783fc9ef1fe7e9e03b9e76fb222d748194b43b3a615fdd229dba946d9d905299fa92c0556cd467bcc5e4fba422bda66939dd5b138b13fdd11c3a96e3522bcf904646c0b4ba52aefd908f4d0173129163be9182d666461c79d5b6dc2685373711ead94e788c3d76a95776e4467cb5a37bacad3cc788b2041a5ba0dc5a3cb2b6c7def5756690cd7954a752d52d5f1d03bb09ad6dc8c96539b5c2a2073dfa3049ba7539f26d6fe5d9e2bc6ff941a4bc9f73f383107c00deff00c34715c48be6a1b5cec3c27c36af35cd292cdfb8fdb860006c4b5961c6f718cebac494c8bc39d04d6f4f56e07cb16062cbb6135c20eb73cfa7e7f63cef2fc57b2e5f07e87d6f5e07e8a3075d6d5cd98153780ca306460d8c1ab8fc1af0a54aa4be05d3f5b47a41ad194f8c6a3c5f8c3e0e40017b1acfd604cb0afc6178c9c4d46b91013712b0d116c6f2164fff1a6949238ff295e196c57a051c8b170024de2846dfe40f80b2f47c014e78897493f4d9c9ee75d333cef93c77819748bf902bbdbcfce52f7f0c53c4bf1270f1d4d9c6c65368fb57d1bfdbe8db31fbeadfd48fd8d863818afaf04f8f75f6c4552cbfca9154d00cbdea55064c4532e0330f597abe8d282b32a457b60cc8d1cb1d4f3a75671a4d5e209596b4f547884924342f788ead1bf9d3e634ba68644d9b88a4a9ee2d17041a234ec893133b4c5ba40868445e3f7f3c690a0a4aec8ff3d0bd514b885b9e3204040c920890443182ec805a2b05201a3caf239f52e9cbbb5d3242dfe4222ded913a4f91e0f73c0fa04f5baa65d58eba4e222f3722c52359af438ad465472948bb124d42ca94a738d7566a26bf682d4fb56ea93a37ccf6c57e03b2ebc900c494f51bafd255d1acea8b48a54ac8d63fa5573dd1219bdcb859a27ccb1803b2675b409d54f89dbbeff1ecf2d093ed47867556034bc6b2ffde144418e5392d3a2f75a0635f0086f514e7e70728792fb2debbd85b7c00960fee6d4e3ed45fecccfd3b359f2fcc00e5dec30766172acf44964edfb27ab2ef575e707266cd6b7e1628be7df6ec24a05c90ee48293877bbd8dec7e347d526c7e9f3cef9e79ecbb73ebdc5bc0eccfbdebc0ed08d1b460f7ae9e14adec6c1742537706cdba881510357ad067e00131b901e60aed74b03dc8ca9404d40a095f52f1eba8020c9045bbdccfe138f012f236d4ec9d2c0375a105e8c3a56b9966b8c5418e02550fdbb9d6db1d9975d00ed1a30e6ab25199df642de7cee67450ece110d039be157481f4e6ffacffccccf3c05dcf95440a75ef23f41639f4a8b1fadcec4b5d1115b914854973cdb4fdfc988a7da46f77c7491b9d87a29290f30940e79c63d26422d53c6ad477c54e8b94a54a43aa5883adca37f883cc6422c8f4bff22a3348232177749bd5179c9811c50489d74303767c59ab60aca02c091cfbfcd4f3b0471dd8b1f4f6e6a834065d850850aa223dc5e10256dcfd48753eccd8b8c925a19b6bc3d09b04f755eb6a2d0d5b9e434a13cb5497baabfaa23a09b3cf56d5d365a9de60941cadd48471174a191de4c64ba9ca204f6952642d365708dd02fdb14bd23a09dedb91c6872fa1346485a2b894857925286ccf272f7e3958c62b341863a78d9a70e3a96f6224b49ca943475465d556e91843917a0b04f9e7f12979e6b1f3a04a5efe92e0db6cd5ed7f68f90f657b6d5456e4853425252e5871ceaf0d30ed324f6f626f7a3f90f724abd97e3f49fa8ed3f4f17b3f74e66bbbce8e9395d01809e75cb4d1d06d0f15403c997f7f18d0b903e00e801ecad6400d2f781ec9223d16a320bf87cb2edfc96eded5df1f9620b809efe3461eca680f333ec39679dde42f399775ec3d58a88d801707e0bbc276b7945093b40179c9bfed8c73e361f3de86ae2ea0afdbcbdba5a3db676d4c0a8812b5a032fbae38e676390dfa001d223d73d6831b38244cc468c2e9b78ca188934ccdd94ae70974314a6d9f20e068c27b754a007572ba4bd5fca2511db1f6b1ffa537b8bc5d7c733c1a6af9dae816579972550df60edf473c759ed2540dd97c57cb16bef1c067757cf964b323e9cdef49ffee99f7e26dd7826d5feb7789b9f894258f650d844b043d10151fa15704b56c08d7da75c60186a75143d05d9843f5eee2a2d4fb622958dcc5a36b14060aa225f6f38e4e51d579ffcc5db4e99a196544c6ee5e33957426e0eb2924b5ad2ea224e83321f5e7edadddb53f3d8ab4f6981fd04d987dbfa695f5f1bdf26a55f9e37b6c8347f6e23d63a6037cbfa0c2915609b0f68975e2f6ec03579512b7be09e0439d70a99360028f0b70479e14b82b4b26c1bc4b947402eb1d0468e7598e2cba2365af949b3e9bca148c3ccabf250b67835a68069803008d5fe38e567b19ae84d0e757bd3637d845c0fd6879cdc78e4c6cd02f86c047a13edf61b0cf5e2ad516eb82892286d75cbbf72bd6df5ba54b8d411433b04cd75d34401c7a5bcf1d65d72d812d49d6da97d6eca9b5c5458f525623cd4a9349a2f54ae0a9b3a169f26f22196d8fc18edf910cb5afe27e47e90df7ba923d7ac3556103777285efb61ce6cf00d8515301f4270a508d8b704c505d47b960bba40da67ca09e2e7f3fdcbbd1640178e77a05ede72dab90fd04fa705cc8f83ce772eb06a4baa8e5fdf456476776f019827701b72726dfd73f3d7bde7e68d1e74b5707585d8adababc9636b470d8c1ab892358087f9166ceecb030434e91ade06540421013741471a75c00565ae60c1265e579d71f2f82263870b019349490b1da84cb0233a037b04a4052c20c37dc04ed048e08780e4039b990ea2e61cf6a8a005cd724f4d01e96603ce5d92b1519c7ff7b97ad301e64fc7bc7f1dad7e166dfe1a1a7fb3b5c54b6d444045500f01b1a46dabfdcbbfe8b422c90f391b416ef5493049863a958f7d542fe05387ee01d56221d523f6138ca943c198bccef7252b3f819d1eeaf092977709e0b10a0f5a3cbfd695ca69b515125faee2c1c1f5989737bfe45acf8c3a528175da698fad75416f11a74295935df3beab2edb572f3132a7b9d76f5ee458bf0d4380209638db24ad8558353411fb6543c95319c9b3e9d6239fa49dcbc640a26cf2a213331a5b07a1de4564751cfba1a4e8b178977aa11e73acae5f23fd4dcb8066aa288fba74507ac341fbeca1d749a9973cf4159d7afe53300b90af3eda2d43da20033ffb924a15495979fe5b5ea3f63c9126f5b2952e07a3b2c36e96b786b95961eff1b79e48f5da4eaa72a42d91d0708c8a872a3ca6ea0a5eca9d8c0decdcb81f20fe5fd87f98820f53f611c83e4c4379c9931be84d2877d1013bbb64489f2aba6f4b3e48db6bd9da59567419db4746621da05b5e209d3d4bb5767a22fc736374ce13a30f138e431504e599cbc258923d6df3434506db50e12ce0dca7773b99d2b231b98ec4b9cdd9d9c3469dee3997f3861b5ce75184fea9c9c9ee3de7e05c8af75cfefe8268df9b37862b5b032350bfb28fcfd8ba5103579d06309c3f8c01fd9232cf058034c14b201290103846df841c1a6a37185d5ce2c00c7e81899517708184586481c0523206b00c75f157193931876ecb8667fbfb35179521cf6f75538776576f7aec2ff674e3dcb1ddc9ae5eb32d9ea05ff2b4976dc13aa22e29fcc44ffcc4d3f1947f1d00ee5900bdafe14b8437ab17419000b474d1bcd202bbd6e7284820933cab0a6c0ac80957924d89bdffe84699f24617a651d8c239e33d20df3c3ddd94d6bf794d775d87699fc82dff40094089abb6f4a90d82c342f8024ba325a1f37754e90a2101f7d6efc1e3ee203705e42bde10204fffecab085d15541111fe0dd2263f6d524f36083ada919b85f45899b4c3b6000cfd52a6679bb2c2669c7ee7055078032bad425dd0897a7913e220e2029d4525c8542679d2f3a326d26ce1f51e63d962eacd41a23c40d43e419d694034a2ae89ca0b1b9b90a49de6ab4b65c36f5fbc2ebc39b3ee2a656fdfadc7fe424b417447b97141719f92d36e55c88f169027aded700f2d698bd21613ad162812457a782c9197c6b80d7f226ed200c9232dfd8e52d28fc50e6dfd04bafd28e5ff1ff23e4cbd1f46d847d00ffbe947610ba8cd93995d6a11a5305d6de675cb3997e5c84351458076d17430b54b90ef9fb17218ec0df5f9364d7207e8191dd63ce5d1ce61fce12d501eedeca3d16bcec2afc9dbd8d9d9bb8e8ef2a88e8fa91559f7cc77a62538bf01707eec86d6ae0ece4f42c6716d5e7479baf7bc4f6d31ef42def36199b463b87235904bf1ca6dded8b25103a306ae260d005a9f8dc17d03df7369361e832578d0ac6beb8d0724686688036c7cf1337886b4e522058183804f632f8c4d36790180599144f9650ca50843f8901a62b5a61ccaaafcc5c45f95dcf6126907ea7c6d13ab7c56dadd29535fe60075e82e3aed4559170b01e6f3f9d7d184676155bf86d6ddecd480ea7bef5f4c2e7db41fd5c7ca41baa0513db84fa0b7858d22c3ac5219f9ea4345a65c236e21bc8a4d426ab4a92c326d43cf4f31797a89cdf77fe6078ce4708279f69567615e0225cf271b9d26fbc8809ebaf32229993906d499974ca56e7d516e35d53aab82acf218194a4376807a5a9a36d9f75047ae601e5d4adadaedb955ddeb00cd42c02de53368335bc436a0b494049d72feb93724cd7999b4f9c60b3d13e39c0c644db9f83da1e136cf71ff22d79397780e0e3cf5671631aba79c6e66ef796c7572e62568d390a62e22deb24a906b87b6789e84c2bcd0d91773f893918d7fa9206d13de577ee8526f157b91758fbdd03fb720d6615bacdb8d293adb65d48130d736312f7cb1d8e166e20174f609ca3ec6fe014e8b4ff09ec2c74893c7bcf1c5e27ece834ff1a0ea53f14c17b4de8fa96de3e161403d44e24c47813e39eb28f77039c3dc067c8759f4572ff9a585253f7d5bc687ac4310cdf7919677fee7ce1d5ce7bcf31d6bd35a3e4b06c07b29f7e4c9934bfea1dce1dcf32ec37d07e1dd6bdef7439a317ef56860f4a85f3dc76a6ce9a8812b5e0360801f8c370fefa5a0a1e050030d18f618fdc00a28451966b1cf078d020aec22680ce6f006b401110456a10d716783b468053fd297c8c69b54550190f88f4a3e3cd4a3eae9b9fa74e08398f672c09bfeb297bdec5118eebf44abfe1c757d2d20efe60026dbc74fb093369af6cffe018272a3d2fb1fb0dd74d6fb1c6542078df49145bc206e0366a40462cab5d74e5961559800431dceaa55be0af058b7f4d2c8d5ab343f8445e3b1a875d2016b00bbc491e7cd43401ec720df41427e7a977695de6db6e02f20937dba633df01628a48de99b6db3bcdae4f1cc841375219ffa92999fb0b35155196ed6ee15b75cbfb9b2a54c2dd4956ea74f25c79ae6e9275490aa395594f349104db9db2e03a55b4a52c9c4bb4cabf15c979646a68d6ca23fd2dd730d117f2a4d3a98ac1b2ef7e1e646a2a6c9548d75448aabab35d54b0747ce99744a114848f394675fd401f2e98737c25e8fead2a6dafec856d7b62f4f322a2f4b448628e953107e06ce938bc5dec768e62711cd7494c9a7b8d1037ccfefa7cefbb9d9fad4743e67aa8aeda806f5fdf0039c01d46c0a6d02b0b7482c173694fb524279b78bb200fb569baed2f78af446a0e1f600dd417a59c97c13407ec9cfc1cac32ff3a583f2f29ccb03f6a6a67ee368ce2a1c03999bfaffd97b1b20cbd2b3beef7e75f77cae6677e5152b4052912229b48e2b894d928a2b29aa1208a90aae7c6ed914b84a90a452b8ca8e1d43b009d24a29906328638bd824c18e0809095964301095005505b30e4246182b22ab7569c35abba01d2d5aed8e76767aa6bbef3d37ffdfff79df73cebd7d6f4ff77cac666e3f67fa9ef39ef7e379dff77f7beefd9da79ff31e83f94060be13cf1895135d612d3787f3f49e775a6e722a417d93dfdd9c5b2af0062af0def7befbcfc923f52ff0450db8381c01781139e80bae7c9103286c1081d2259f162086c1266847c56aa36c833fad4a7e1cb02fd830fb001658d3a6faf44f3a7aa080be6778fa74121f79fdaf7c65b69bbde90a7c59b72daff642bd1ff9911fb974f6ece44f0a61ff033d1a9e90961dfa2264c1a3e2c81870c1027c409e86a283c05759e8a39a310ba5a9a67f0ee330a5018d9107f0a295ac2907202bf99c969701944200ce4f15e28436eac18668dda5090b6188142191876bcbac658d597456050d9831db1b2cc063a512cf51838827967ab05428fda201f698af6c60185b0c54864147b2804a26cd8dc07e4fa9ef8b97a8af46d4d016f6ebef037551c3804bb1cd30afd287b202443503e5d99bae7e3d0ee6c920385a70e13da7e5e581313e6a219ae6cabcece1571ebabb32c7a8e5791a7e555d854cc963c1568c89dc9853fd3f82b79ebf2b00fff57d7377da6926a59fe8037b5888501c921a93de63d6bce7eda15bb4402635dfd33cafa9dfd735f76b3225d01e5e53ad6b1ac3550deeca603ebb21e8beaab65706d3f9555da45cd35077f5072781b7e34dd46c6229589684c813e6cd4dab94d6b492eda6be0c9e9151f162eafa739d9273a02a8e3ee34436eb224cad919b26eaff5ee2be4b77350bfb82f72d5d08485b176ab9c3c1a4092af725b9c2dc7c2cfdb8845072adccba1d47c9349fc6cae5ca879ddb13c9db0b37c184decbf2817143508eb71c28efc940a5deb6233a8f53e3b92f260073b63b01e7d57b5e8f6139f7f7b302feaf7f3f4f20c79e0aa4025f7e05b881545f6ebfabafad87400660ce5c25ba00c60c60060be52b0f808bad821095d44484617054a141874cc88532f248780baf69c09f8b05510169ee507570b6db880efaf3fc37eae65087b34c0a4c78b51787bda8bdc35e460a7b6970a87323a9bfbfd5d46dea8a2f972f5ff617eb579f3dbbb377eedcb7e83bf95b1503fc6f09b677e88bf861b0cba12a9013a31601928b064c3c66c0bc8ccb1ebce7ea62d5330dd3963a540b0d8148b7a61d3af94c15f08aea1fdd3165ecf272bb921b59a09feac8be5bd094ab04ea2a2f6e0cd5d1460a8493efb1bbb2577aa1af6846c5e8c7213018073b6dd2b5dc97df177a563ee130cc83fa3e67473b9d87cf5127ac1e53eb686cbe1860ee1eb7aa963c6bc6d8f57b15e351dba20b7361feb68d79a5e3c22034a0207e279552b516d099023682c4a5939452dffad18b4c55d6817cfef93d8fcab2412dec465d2f911995554dad45c4fc6ee87751f5b4296444fb1b6a72431dee09f205d1ca9bcf77f5970ad8784fa6ae29665fe5c3033583ecf8fdbb21d5aecd04dafafddfd59ae0d8e1f7f5da703613688ff766dbb3ebab59d1bfbeaa1a10ad9b3222a5ec5aa242b635a459b24b30b8cf80e435b56d6961577b29945ecae26f592b2b2e64f64f162f0afa256bd228c4b2889e6a4be41a90487c4f1272284dedd7d6c9c15a288f8ae12d2f8d561c96a09c1a5500d7dec26bfef060f045fd8de2611d5f7db55b5af166612db5bb0ae4f558f3f3b8390ac4ffd6cd994fce24154805be3c0afcb060e2a18017c18de1473bb67e9a135146c4e8822de6a2ea1cd419e0a63a029b7e380379513740c8f4846dd14b003a6911142048739faa4da4f72aa4f3e55ee122d677c148d976f4cddc6cb54ffb13afb32ca39e0618cb325eb97265fcf0a587ff1d3d4ce85bf786a36fd650cfd113170be19565d852c0c3d768e5cdf654ca39705c0704fc01db4c2ae2a7752c20ea39d92e3aaa8d3655330073a499c954b91536e32f1101e0324385804dd1afc7a61c4013ad004acc72f9e0eb29494e9fb4a123bce71e9b2a91cbcd9d5e954528ad9538d44a46016195b13146de77e7784ed12ec62e402efde1f9659cbe10e1281bc0b32f16e89731cb284fde642cd117e3a53b426494cb58552762a7015fcea9af3c01bb2f36985ca9e372d5c2e5497fca2611a32d73e6e07c6c503c18eeaad3034dec86f2f7d497e059f71dcf47ba1172ae90908068e5edcbe49ef4b8361fcef635c85d35bfa1fb33f6d5a5f205dc83f9be345608893dcdbb9ac7be6e6a15948fae0fec880d609d6a807a0457a1388da0cc01ae630e539678ec6de1c28d0b1af4d4756604920cb70707430d5d2ba2b493eab56b5736e13f8100db8e68a58fcdd96ea1da11ba12cb15165bfcc73a9ad9a71a675dfd24866b331ec4c2200713dd205accd63096c50a479f2dc0b0aac6396c2e8d3b17b9719ccc00727518bf2f6a80979cff3b2e55b9c257cee97f3b6fb00e3eba8841f6b60eccdbcc3296f0996f6dbd36ab503e109cbfca85c0abf17ffb551dfb707eb3b0966530af71e96dcf99d81805eaefe1c64c2827920aa4026fac02effbfeefffa3029c5f1770eef03518f054c00d0a024021303e6d746ebfa2e21caa6fd3a3054ca8a38d64fb0da9134212c834e415d802dc04426e82b714ab9cc4928e585623e563c8e0351c7c934eb44044f7a7f9be475dcba46b05e31d7f21576f7a01f5a9ceff7535fd8f05c57f42b6de84c50acaf64e3316baa74ba049ff7cee94cecab73f23765b9d3332a64b51802e36552a8804363d3fa03a66e6fadce86920d759684c7dd9910d2c4637d87186b48804dad893add3f08ebbbac615ef00763d2e433469a16769cbd846dc54ea31ab16f3f338b1a1a5ee5c105abb0f5aeb8e505ba42d03b40d0c90e6d7817ce6a671618fe1968b9a5a9f4cfa921d5d3de9a12f03856a8c07d724c0546d583904660282f6e49ddfd79804c68dc0592be1cf87afeb38558f789c293b202d93bb00aeea5fd545e081424d70af6287bc3d7975af8f46844bc8ac7e9ad1680921239f4e8fbbf13bb4ba2ed9322f1c5d53a1d72cfcde130d6a85e7b9d4ab560295e36ab467e250b2d65781bce31e8da8f9e6edaa21b7d005c1545708daa2db5ab8f6b8de0b1ef63498b6ad734a36e3ea75b1fc162c9fb736fa09bdc7b33df9ccb91ed78f37fd6ec4354f3957c88c7e27407136ff6a0c767530a47324b790fa7a287763ef0073f9cadb8c575f7d75a13f0a80731e44d4562a893e7857285f3e2eb7c9f3cd54203dea9bf9bee6ac5281374c816634fe8070d1ee29a0cb7029760344412dc3390520257902420a298f0ced5da49d2ac073402f1b552257090719f37d267c731deda8401fc5170afcb98d2a50c7ade78ac95dd8e22b9f7de9c6a58abdf5bac61515b44ce3b7a8e0bb35d2aff3b8d48feb03b14a8873354ef543f7dac8b6f7dac5b5ffe8233cbd6a040ceb5f2c1718171a311fdffe8715cf29861e736542f69cd3092674706dfa91f79b797a480c8ebad45326ef03deef4815ddc287ed3a5e61c41703d1ce6bbc605f1e6abcd3003c15bd4ca1df4cc6ad77546fce5cf04e043b65f5c2821edcb7af5874221b02e53dfd95e19afe82f2ba86744da2c96f38bcd60c668a879e5c9d297f321abd2eafb8c23e46afeb318c57b506f8ab5abe705786f79af958ebd0cd05690c4cef98bcc59cb5bf20ea4630ac3d71c9113c13bf57f11b413ddfacca70e4b31e39389a986e8f54b9b11177cdfb195b7c2d8e9ae90104183e6f4a54217ea2daa17da54a0ae2b7e81095b56d348752bdc7a66de96262a2316824fa996830c07a37a65a93df66b6f6b7d769726b0ea5dd56eb4bd2ede954b2ca6ef73bdbd55b4e556b764ff33e1cf06b171125117fb3dce278e7753c5bfabd43f66dd91433ab1b1d74be6e1e8bd681f1c51cbd65e5299fe4f37f6f5f6ffdf6d696636096eb462f01e8b50c489fcd760eb8cf13e33bdec5986a9d7a6469d79ae618a12cfa95ef6dcb607e5ccf7985744cf521be673a931baac0e2a7d5864e32a7950aa402774781f7befbddffa160ec4340617c98c43ebcdffa1e17ec110ee2af48e01122d7a65cd00b9e14510108910f08c5d769391a10b1c3b9fae0a813ff234d865e915fec912543d1abf6c3e173dafd692cd8a35e9e5c22a7eb743e9f1c0006a3d19929a04e1d7dd97f9300e4cf6ae05f8787378055466dcaa3a09a4264348702ca1e0663f7787d798221b8dc212ded7ced257673558d7aec696fb0361c03c0cad5abb6a785a5416567aa4c19c6fe524f394c5c3fda696bcb55cf9e6afa71016df5f27c28545dde27ace9e85af68aab3273509e568f79452d5e922183b63c9157d5ecda6832b8321c02d383abf2e0efaae6558dfd4b5b2a175d5d2784217ca175af9a457f86c216a7016a81c8ca74a23db337d5162acfb9656fa77cc228966df76ad86667b14b2dd459756237b6fdd9ab4a0fe715b774455a865cd3872aab801550eab48ef268eb82c4c07ac886333c79fe22141a1c65a835e0ce35b663556e5b1d4e8879c1de806a0e3eedd7d3efcbcab1473efe6d51b0e3c4b9de97dfdb5991ee7bc05782b8d727a737565ad11fc6ca7e6bb60ec65d65e56ecd5f40568e7d19ca31b8ec31ef87b2acea7019b82b8c2f1f57b5cdbcd3a3c0093ead4e8f2839d3542015389e02ba79f2bf92fbb5c021e4a797ce0af6d9fb1a2e67f95b016c485147f32c3b413c2b6960c1684d1cb308301094aad803346b8e4e0c958afe1544b64bca01abd8e61596024469a838625f2ba8c42b42884b86db02076e88e30987420b41ba1cbb837f5fe1d1ff89568bf85a01448c88316a8bbd87ab280d8d166fb19aaa8dc7ee4ad4e2476d634654605ee4c9078e979a46ae5314d25cc263ad73d5257e3ccca892d2f8c4bdcc9e4e992000cfe68345f4a975a224caa53576658891a00707c7a8a39dec56cf3fb91ae1ee703efb9c7ccf97153af4b28ceb4134832f2a34e6b23ce89fd37caee84154baa9d638e9ef0cc6198121f275d385361cdbc34938140fb4c4c7128f1b562b90468bd8075b06685622c287dcdf2835ee9a430f5b31a4f71bac4a77c65795aec9532386520e54aa666a834300cd2f85b6d8871776c4ec638a8b58ac4aa359c4d0bb4d6d640b651702f92f077a2f428a7eb9d3bcfbd3c136bf18bab0d0d5e7a11aab32f6b70f02acd7f89757b5214fe3900c85cef9851553e300d7c59a2f6b564d639dad9a3f9637fc86af19f4ff1a03253d9b8c668bbf0dd142ba7bd4b359fd7de0772fd235a7da5e3e9e04c8697b1c28a7de3298f7bde594df0ccca9c356ebd563e4e6feb42ab0eaf7ffb46a91f34e0552811328a0955ebe5994f6f5a23e1d0065c050370102d7d09bbebf235f904ad80a405ef25d93766ae32c15fb51f57c41ab1de5e6bf02a3b4833d004cb20051c7ae33de022560ad715f0d03a0b1c336d76a19437f85cf5ba2136096271a0e9be64f2814e2bbe6cde86d7ed4bd5bd132faf45f056c5b5980ae4b94543fcc951b2f993b03e6c838185be439611dd4ccf3658adc6089a546156d91e6aa6acfb712be4091ad7a43ad63e199383a3002fae3a7f6639b2a218382a8a583f2c822e07638f88c5ecfe9f4777599f37b0a14f97d15fe812e2204e1db6ec572910e33d9d2ad8df3037d3f6c79393edd53ab74214d195b0b42506cef5ba53026fd7be3daa87b4c4dcde5b86471c94eade95add306af660361a053ff796d62cc119f6f40ef675a61f4bd1eddaf6d85d4dc0114ee33b59b9bb505bc9516a3f5075bc0d44af682e8ea45fea6deb5d8723f52a1caf93f59b6e605661b4f57b2dfcf5efd3da266506ba3a72d4bb46b37e4efab5383898ee6feb7743dd30707e9dfa9b3cd7a1673fd3e91addbd23bff750f1de073e52b46c833cdddf11576e9c2c6ddca41d59fcc2e859c01286ff1b6c15c24bda7927ddad7e4f0e5d672d983d2e90d36819cac93b0accf1925367d95b5ecf294b304785dc9615e87da42e17e5792a900aa402eb15106cfe2583a4abe89b5e10a948107dd9029afae206aac315aea30830905405fa3a96ebbad111e831780b9e62c941151b1963cf37770be7aa533decd1811a57100534487310fc72a4bfa087b9975c30984325c5d52bf0794048f01e35f84603025e6ffd632592765c32e432cc291d760b98bbb6d28ccb732b804d0be6ed36f558da43e3108d7eac9104e362c5632e42307a57b15dda45651fb029ed18a74da92b2dc747a60150257b7a4f5ed03bf119d5794eeeff6765fb395d1a5c6624acdb6d84936d5392d0c1f460e454208b28db12b5d81a679cf6a48b5265b05e7520a10eed165f2b86f4362f125a8dc41e574522ab59d756f724ba420d8311b6798006f328713741765d596bc1a1c9515b704876ccaf26c48b0bd1c3eea86dedbaedfbecfe7a3b5bd3b9cdeba49c1fe8b704e53dd65ef52e19a0bd8fc206efaee4662945680f6663425de861dd26682edcbc278ff6784a003e91f87e1b95e2dda44e8490f4ade80edc81517ceda4fbb523adf748c2d7f14c3dfba94e6b0eb502ccbda2a9a055bf5be5c2297288ef9617bcdcabe9586fb52122bc6495db37b174f36d0d88d370cd4546d85c05e394bc7a46377a5e8e3aec1fd5ab9e1e07ca69b30cda15c2d71d57b5212fb754a0af009fddb9a502a9402a702205b4d2cb1f9f0d47ff776148f800f40a685582ef7f433a479505af0b1da9a71d5e6a6fd4559e7e0ca4dd0792bcc9aeac7a94cb405c1404c41a55550eb002b2b4abe08a87da101cb994bfa0b26fede2d30fe4251e7fadbc863f28eb6f3328d3bfe15ac04b63a5c33baee61e9aac324806cba4357ec643b84ad428d9cc8b2c57635ca5be527852b5d73895c75ae12ab3375d296fcc514995da58bdb99362e2c43d2c5a61521575be379acd3f25239f5254cdb3cafc8c1650fc9ce6b6082a02cc1e136b1c3b0b0e1a6e508cad4df8d45cdaedeaf58dcaea127bd1aac2759ca9b4d786bc386f4b9de8aa3885d6c6df67e861bc00004000494441543863afd3c5866eb7dd3e91262e2faad5ee28c40d3eae57205d5149d9ba76ee2b5cfc87eaaccb5088d4a2b6b5620171c762d4bcde11e8e686cb75e5bdaa6b923d285f53e3a8ec80eca36a2c969da4be3439c1b4ba1b35bd9a0add564a5792345c05579b6bb385d0000d042ab8530eeeee104a8116772812dc1d82054d29eec58abb3bc5ddddbd38c5ddddeeea39dffd0779766666cbcc7a856a9d7646f7f02137be174e4a84a1757ee5ded84b1c2e07fa29939c48fc8b5bdffe448a3050b409d5db5f01342a57d44b3b9587f6cfd4339a6b478c18ecdf010779bbbf24192fffa5cb2350f38ef0c91d3fe2a08d7693671c952d94227318942cdd2d175600d475f415b863ef1408e5409a45c27c04740a38ebd6a9d81c1285141eac0074c023d3b9c8b7b64458c4b9a2e1d1e52f7649b181c4c1932f543d7a07a64a1fac083534c36ecf080247aa3042394e90dd34914f8f07f0df4c4434f4cafaeeb69ffe54e6a54934ca163d8010b368fd2c9b55ecc83ea92ce0ae654adc076c2511f3e0925d86ed7bcaaa7bab942b002248c7f0da8ffca3770dbb2f6de8724fba5e3b92967d50b0468314e1ab2da28e6aa6e2373422f16b06c064890bb5cb6cdfe3c455f5089d03b548f06a67b134cd89220ddd1d9a1c58ab35855ef727a0798ec825dea6925a2fafb410f033edb91e24a36927a18db108ced1be6f48f5d0bdfcd1777ceda65f4acc6cb71b30c44972609320806bee7aeb79c862576378e7d1bdbfadd1d788f6754ca2e5b70b4ed0d4f5702227f24620a62e99ce11f21d9516ded91e34b52ff6385da51a73b3e6be2a21e551cdbdc69b4c05a1ade66b97b5a48cf88b7e2d2044eea50b94799bea2ba7d862484f73196d58842ae90caafe11553e08b59f620216496a957618d751a910ef38f37f9c9cbe4e5a95fea4b5a358e2e7b541358c1742e85719d2d5ba100477ce5ee0ed97f1d02d6e7e34dbbf16ccdcb0df2f8eeec53fda503a14d2ec73d81bcd5fa2725c75fc8e9131e317213645726538fc98ddf87c17e8b25fb2ef8bf0b5c597e1761f30c92f32e3accd8c3d3a18c7c4a9e5b8950bdf311278aed59f577a65cfd4ade2300b59319dd651dbb5d9fc032163755f12dc3ef26dd83016462aead573c5b5a911d45d2e9b738e2c6e6a8c3dc8ea8a6a1954c1a291fa2c85f647aba9d1cec5644118e6a7fdd58cffd7af22b461db836809dadfb64b7456e1e86f66586649f730773bd78caf1bf3a8fd7b38fd88796d53da9dbcfafb43304f1f539ae79ba0b429f531da48d7541dc9f291cd518d1f18da60034653283da83965cccc0ebb9db9cbf03791211ae5509b968d48a1f3cf5ffcd6847158f3150e3e1f1b458a7a7f5ecdc7d5907fe6f0654d6705b32aa820048b209ebc4b4a31164cb89f813c5f9111f70aa0e936520ad4822766745ece3dc09f0093f796edce57252a09d6fa789fb7e6bdd1953286f15c246a5d62adcce6d1ca96f93aa500df105a5bc5f482e46cbeba9112f4a921072fcb03f6b7dc1c67e2dfc41d08edb1a8fbfc912a74fad65d442243df68c44868bc595e8463fed0c1b512c2118c1c8a1feff12f8bb499591b8418612e1bf738191cc21df6914f2f873a541c535f50ee3a7db22d24cbe7e57fab61bb8f702ae243ecf85bf0f0c2c82313bab8e1147af5ee42a1f5c74897d0066be0ad7ab6f0b87a95a093e5be579d53366863e663854b60af8d77329fce2c1e3d7b0c4663dadc8603525aa356959d695219f864dc41c65512da245e8b37c23479c1ec7b99d590cb29762f27e6513830b1d6a2d3e0ea7db450237ea25d5e2fb782d959388fe6302a48b0633d831a87d14b2e88b95173cd11cb9aaec1577e75f113d4ac046242a56e556a3115fd4b007a0c149fdba6874d232f8cbfd8fa5a777122e9216876328ae755b4918aec506708d36cf4c9a026ce6f4f79f6c50bd7672266e4911c18e2afab8176c876ca6ce5833a68d733b9df5aeec35e0bb2f4b95c4a2d0ed8537c2617c95c10ec0ec27093598d3d8f3c6b50693cf54718cd55ee28ab8d0791226f344cf8b61a1dca86dcc56c00ae4d5a6d55592074916c44b9bb1109db191322c57c976df9995e9a3ce58ec806e19272279c8d09aa9c1bd56c18acfcda3d072c92c224c1e1c7d7a155f3569458350c41ba13853cb6a5dfe5aedf3657b6bc817e000fdd7d505da42e5c1ec52aeca9b2741030a18e2a94797d5110b5eca268cb94dbda19b78fbebc644099fb88796c6567d60c33109ef2383098be787648cf5286438173f316827683b35ff82397920fcb0b27dcb9ed176b7457bafb1d42c6c0d7bd5a5009ed97de918d6a219d9d22a1dd653dbacd23119d2d027e330c589b6758a32a72eb9580fad106e4f6f72fb3b905f861659f7a2ba182e853b5cf5ebab716d411597067da26b2e78f51a444167cde6a5746b8ec880f4319d19403791a6c90d99eb266880259ff009c78df78bca4c40b3e1fd4371ac87615ee72f8ce1080327fd8f34bad919853a37ce6cd209a8a63b9fade30ed3875da1a98bf08b5f4bcb60c2225c6ed16380c704adaf397b3adbf74570da954928f5b3fcd96f636aad9a0b4345b2b37020c56526a8721db896c18ba3c124cf6708b93396e5a12c52c740ff284458ad573cee42217e933f5ae2c50e2c7884b100d07b6654d076e5e794bfa2effe00b2e74b19164edf1dc5717d7ff90028cb70a5e547b558a2a244f02067432b937eb2a350dbc1af34012a2b01d3e76a9498a347e1f5f95edb39fd55ecdc83abdf1c9c8bee177487b2a15c5c130a1adece1b2ae5bbf56c2c4ec9f89e8baca54de549465027e91c283f58fa5b575135e4b6c16281a684bbc0d752d6e4ab00f8bddae7a932cdeb33cb8bb2c3a48a5a5f58a6f349fef0f0efeb4e133b0a63075b9323737d036cf1e461c409a67cd3b3c5e77d38b1695d48e1931d897d3b7cf49db164218a240243208da34b71b3b4b5d89d254b21970e33f94264f5176a2b511beccf6fbc9ff884175d6902a0ec6d1174a7915e36549e73f3ca7be5ffbcdaf098a8ad039e91a820e56ca29136f54a1acd81c1dade02aa7734216334097283a5241b22909bd9607efd4828955975ff9b50bf99667a25c5d8f16abaf06ec94af39465c0f2e1634df168dd54fef12065ffc1a7ceeb79f67393cd63ab2459e1ce06e4c309a16209904e0dc9007a9346f2b2d3f103596ee08fe46b4d4cc6087f2632e7c2f3c3cc136bbf67711dc7640bf2cea52c5cef87a691e923d468ad21abeb85cc6bb7f358035b11c9c664e98b68388baf923110f42f6b2b313f47051e0e57b27ec251128f347df7c9ee9706379c1822590abf5bd57cbf3fd419d892b298cb3561bf9e439c516b7730ed96de4e4267a4f75cd5b05b24af43fc9e804ac0f8615247f3e22cd17680f91fafac58fe9551998958b50c439734dfdc552d812a828f55040a02f27a7ff339ce79193a4e9ed9e9b844fd13f6e11d40b36cb5b5c6839cbc50c0297a815b55f6b58d36cb535162ac0b00372a39b1eb4a3ddc15a6b599e2e36ce6045624c3c3bfe2bb5706ae286b7b87975c8a35b4a09a9c2b83ec66c442f768e6ca26d8777ea47fe3655ed5ba97acbc5cdd3493dee58291a1cd04f177cd9605fe87a33cf8ee6464fa03899c82ff4fd627d9dcc7735c5f5c813bd023ac78bd0cf68528c07fd8da5810b6ecb4778b2570ab0f796c4ec53636da9300a77cdfaeef088ab803ec0ee1406be2d0aac8dae0bfba0a19cc4e84099db8ccd6d0d8116b1da17ca5ab4ae8dc6babfaa1df7e7b0e0683d903ce267011503bdcb539e738479ab228cf3f658a34cbfe5f8f372f6e07d2d472ac83a04b0ccdb9e374cb7130dca01b8ca20ec2e06993ba74ff150ca26d4fc0d016deebb4aed4ca72337c26c244b45792b9d28a531809b32ec43a26f36cbe04e96f03379a3c2f942a3f6a4424da93fed125d1402677b65c389748443d7a6e2ab446153d33d5c6976d4842a3652dc1c54c681d2960ad4733955da516f4d482ecfef4f9f6aa50f3eae13e23eb55b73f8b864d0454fa0778a90e4650d9f26cf296cc155452ae4f889d9f7e0f6e3625e2532608b75740985903a9d62a54630e334971cdaba1752642e00b8771e9f7e4684782b254436872b19810e5c17e9c36b17e63ab9c7ce7a3d7bdd5a3c2e53aafdb69b57cea5a956e22bc5a79d3558ddf2f00313d76c2f54f3d13e85dfaa076be2f13b1369cd216564c63fec7cf3f491bb0da541f8c246a137cfc81195ce8549d9a7092d2d1b7e55dbbf9a1e2c9d5cec5d4e8b644b2257b01e4ace1d0517bf7424b7ce765c393e373a5c3d94ec6d6d34d626a683964e40a7713fb4ffb5abbc6a10b2abb1609555500e6afeb799db58c65640972f7dfb2006a4574aceeb88de4aac4199835c4c4981cf62bcf4dad72acc28d2653a8b3f595b3fdd53632a42842e13dcb2efc945a3306c146417ad93bb2ea6d1e97c6492effd3398df9d823a126f0b5555e8e0ed73d620c1382a26f1f26b6b571e614c68740bf6f07657a462bef6f9b8d921965d3b7f8e2685d7342cd2714e473bd26951d11edad1ac4bae233baabc3623c612bc990a46d238aecaafbd8aaae389e31b93fb61559b4afab926eb3053ba954f7314dc0efd7ee09dfa6b0e87f369db7bf46c0a5b084287d6bad9d4948a9ee621de10b3b297f65b5b5abdbe25b73777824367f5f8ee288c7a310f902f86481bdbe692e5ddf909ca01d465f9523c254bb47e8339142008c8b8c588a33a5624d7302168742a64e3d250b7e9e50ffaba2e246da4c7ae5ac508f14fa84eb6f3aae49a1912e01aca3ee120a1605757bd69a524673d5f17c139f050f7f7a18f5d04eebdf911515cbe1194733baab48314a1f8eafc37e02ccc2f3c354ee43a9f6fabfc1cef3ba3ca3f543f0be4ed82a81c8306fbc72c047ccc66c3a04ea399ba26c73dc9a25635c9c88deae2ec6fc6b04ce1650fef3c4ec0a664a8156540a52239528d8feee5cdaea25b351d1d1d2ea4c6dac09d2a0a2feab0ba38e9ac174522bccff443d94c1a17778761b3443e5e2613d189765e07da5b5e2bd18f50ecedfe469c1599f166db0a57409ec66fd48281b6457ddd3e9fa58da24a532843140806ea9e93764d2b54a5c0fd0cc4a2a7507cf9c25e322b1fe8022cfba02cceec865122594b39c798b776342bfaa70dc5c522d750ec92eedb203df79af218f9274516d166cc8ee97cc1fb6c6c84a8b3052b689ba9d6102160f0e54acc9277469eaaa720e42ca4af9fdf3041daa4443a21c8a9ae9c92f3b2233c63e5d5b54e2cd20544a4d6dc9f700ddaa75070423eb977513626cf74ac2d892dd14c12000b2ff7d7df73418ffd9a68c213ba7585c3c34138628abf1383c8a20db0fb518b1d86fa0b0dc73bfb62c2dd4252b507f8dbfd331c3aed46ae0897f2f3d4a56a29ab9eb54ed79b0a2b930ef4ba6a29bf39a859b7f7ccb0ca2d6e9c7697335a9d320cf128f250b342815a25adef540fafc838db7a1194bb0ff56d9484ea2eea78dd5a5e44c03982a61388cb18ed6bbb1a878217817bce4784d78d55ce77aeee451622b89f81119948abacff23851cc089d79fe0310cd7d56f73f56f677adeaa6f51bdd2f4d7f5f7f1e5a72b3ebcb52fa428ce3e28fa00d1b5466a508a1642b68b728794dbf853fee903cf73577ff2baffe6db7ac4de275b93c995da940eb8c96919c07a42daa9daaa594bd53b8fc52b886f43dafdf49eb441e11ead774fea854becbee022568eb986ef85b1ebec36c1c6d65de367b3f8058b84dab23e03245248b864ca9725d96ce7e24ca3e8b6986bce47f2dea706fe6279f510f763e3078bc6283b0d2a4dd4352c7536a3de7c4d38e48cdd9cc182d4969a0a4b6f8b4b3df56d220e1539fe3f39a56f39674c8fe26625221631039a041ec2e4458ad3d288b1fbc68a7e8d81fea4bb4f25bbb4b111084917d72c16a989db3ad7443581270e90aea1237a8c70bc1c4061bb57b1af68e0c9e2c44ebb0a4275ad8cf1b4f5e4e55974214cb28ddaf07d3bf0c97d4022433ec7ccbc747a87e4e871208b94e2b3166b3b01d1f79e113fee430913e438554756cb3736983f81cf5f887b7fabcc9cbeb61fa4a8a4b5c5ca51df55d7c295e156433a3e0b4c087d1d81aa6c1d770d36ea8e1a34840b4852b6c488beebff55622cdbd9fef0ed38f129d4cfe1c2137114df29d3dfd0d114aa6417cadd7a172ff41b5461bb42430a5cef2a9f8d429cd559b8d91944a4315ee89360f6008af452f9aa02435f3f5dacd2d6df2aea88389910e918df768630d921ef2e5d86d8b0b8e874575f8dc51b631fccefaccb52f57c00f7459b8d4c24dc7cc7aac876f82d7f79f60dfef47350a46ccecec236bd33bb1573a70f78918cb85d4635fda749c5ebfb7383025019a1ba660c320ebb1fb81c5c286c2f54c920b47b4a2e59994497efe0469a859a9e55ca037f1c669c507023453a95bf41f9f4403bb884843e2b8a42e7fc1b5641782e236933d1c1cdaffa8a7623e8ccd76174888d805abfd8c9f5b891e8170ba690fae85555c2ca860b5be862f74f63bdf0c58a54887a5d16e95d552fdbeec6fbfd1756ecbeee2cde0ff63f9b511c1a069ed6141d67ac659cdb5cb87eb63af111b4a9a0b6cab33146de79f66d186ab62251503e90d1403025021986d5cc16c6bfb88bbcadc80e66e12068b361f34186751f331657af67e56586c163419b169cdb000962038923601cabe2785eb39e66a6ccf60907f76660e83fdcf9f1de2b34472b79188da216d3e9bc4d1e1ebcde561e1c77e0a7df6b1a97389391070682366e9182ba95c5bede9c9fa113979a5f679e4722089abea66ec4935be97fa5c7fd0c544650032f0792b4ff17d6bafd113eb9e3469e6f7b1b1444fb3583567da38b2a6db0f05354fab9c7669e7419d538d792439e5011d27d04b22bd0a8ce2052658a22cd602e9ca30e1978f60603c40da8fc30cecfb04e3231aaa8d040bad26e6d207d439ad7f53e0cf8cc0e3b727f0d797d626ff49b7a8c9203c22937c0803ae4df1e28b571f7f01585ba798b34a358c06535f3bafdd6e8cf898fbc64abe469b30aeb0726b50edb46f0b849b9cfa50a52ead97a479e88e0dccc84b8ba2ea7130e1e38348d250615cfef6e024f0e768944b8a1e98ef8a39342f1934540e798f28d7a4ec76d39bac8a4f2d563b6b7f1d80f7649b3225fa1c9a2c189172efc1b21a2961cfbecf4b868eca3e3631feb5433d2db16c21cd604cedcff929c792ee0a5c5ffe029c61d97cbd1a2f7b80d41b235339f2bac90510c50e1a6c0b363a1a3795449a38c7aa59fa780bfbfce8c87d12ba9e91f3497cfe02a2b805b99e90261e2f95c9751e444349f76a4cc3a3ad08dc7d3153616bb4e3318c38c9e4c8e2afeedd87a7c68e87609128ca89a60e121c5b00f18f155046f64ff9a786c416c1f6277df6c94cfe8bc985c9df77537fa7c02bb2e652f70d23ee4992484e2186e387cca4e11d3be5befd030b10cf54eccae195f61df11c97e6b944d2da59655facbc2b7a51efdc3d4e911bd0afee2448aa7c7fe32477557c49523e9cc560b6073f97d6f16545b0a2512839334d8293fcdbaa217ab96cfde60edf4433846016d0c35e7d0038b39f9737ad145e61cb4e9fae6611cda323dc4d6eac9e2f7df24af98009d0e24fe071323aedb89c7ea03f3877e3da4c679ccc80e7b809c3bd48fa627702de523bdaf303be90c78577a5e4f1b49a8b9832112646fe2cb508b2c7baa468131a5445382d1344817da63cec7f5416f6b9f115315479d00cdeb6190e2709c90eec0459ad3d4f10fc04fe43212d960dc2262a1a058bc65dc7cce9f1c9c6ed97bd1269414be52f4d6a705f399b1eab44ff524a5af6b74634ac968a6da586d63eb63c7dd659057aacaee633f55fa44835d9c470ee52bc9eed1439d819de33143e030718a9a352402aa3376123a5a899d93806b09ebe5f7883b232bc2a427c0cf3c8649054e3bf00eee901bdde89173f9b383f71fc4b45857527d5db58a450937fa255e8d70b3dff4f0e333579dffa87ac95b796a2b1dff796d908a3b4fd8c39133e734e828f37764e9abe0f6135cc9799939410bdb4915f545ec5a3ba0b69a986b7fd06592fb904a7e683c7641683fa34e27f50d36705c6593ee04f0f3721777165e1f0f1a91a7393004dae4f9526f11b4ea46e6d976c9b7f0951211cd6b46d9794c640eb5ebae45271c294abd1812f8d2df0dd24b454612ebeec598d6f634c338880e072f5a7893c02f65a32b1f54727d34eae44e96a89f0a5a5c4440a6a4f82bfbe2e5ef54ee62ebd3d0725bcd55fab239265760fade98fee195b377c8676fe573b2f74ff1a324efc3e2f486e33108b8610db0f0082e54c26ed34a056d585f4460e977d6dedde4dedcac07b3945a3335945dddd219d9ad6897e1e66aea251aeff26936c3c495acae07e37d0fbba5fab30d6ded11b4de0da736ec302390be1e2e96f8d7ff5cb67515606bc33b26ffa7d35fc0b4d5d6bc1484ce923f0e94f33284f6deb6a39f3464608630c20235a6d724a48a5e4d031a240d85365342259f10e28248da6cbbc9ca711674715eab4c7bd2ede9a3c0a8c70bece56845bdf9a9faea48a730c1612af46a7f0971d7696593c3fea07d22450bab7ecc206359d54c8730ed597adac8c643fedbad345d792bc8e2b6a731ac9a335e92ead0dcda768b6b664f029f36e5604bb9da81e0f582818a09e6d79a3b4a4fdaea3f5ce71f477f26305812b19cf9166d73507934d35efa931aebbf3af64f2b4917b70dd1bd119e24daf7ebc0209a36e9b8622c7256c383cebdd19b6aba0979f63c5bc8216ccd8b7329b3b4461b99b56fbb78fe24edb4481cb923d8f8fbaaf9bead809a89fccda32885d6dcfdb11f1ad73386d687f3acdecba87d4bd25bca2dbdcc9b8b93fd9b7dbbb03e33efc033b513f8550e2b7961cc7f3d999c07b9496cb4e01cbafb2b2d6c1829cce1c3e30c805797cdd3e45ce80280c585307f5fbd1cc0bc843e52de9e720e3ebd58de3b3773d248e610ccd852fd2f36fb0508e2a9a86a28ae22fb3f5436f3203524bbba1c97423bc6aabcc632e54342d9d4590a5877352b910e39faec2ddb978feec8b5028358f800a41a66862ef639b7e2abb12182c6e191cd7fb6cb51806b8415490d79478e5942e2f0cd29e4ad95eaed2c0281fb4f8cbcd72b1b4ea4a89bccf73baf4bdca674d5041b0df836d13c03856f185bb29ccd33b4cacd4bda8a0f57a7f414458a9d27441955582d8fd349236958e32316506cf275a81d0a73c8458f55b1f184f39b087b518e06799247340f7b93436a1a2654238548751a71eefc3a723b6aace64ca14cf9c695291b35bf787bb10f4f1e33ef3c1f48e7caf57c7edaff6c7515a66cd94fe8fb4b0230e0bb69de27d2d1bb5c405d81afed3f4f5d9d4d4e628100b1a58a850fb38bb274ffbc2afdfd0df3089840fbfb4d396095429a58bcd212834dcd7257c33607b3b4ec134a15b807976747d81e2c8f25e876adfb550e0021f49be4d08fb0e18fbdc7e96793121a557d9328b9d6341acaec91aa16937d6014ca191e3de1885887fee6ef8bd3c5c574e6d11d0f201ad487e7a809c3ec05b7cc3910abfbe944cfc855031ac1a3dccee4ccd1a35e860659bee70e84c92de7bbc5d4a6f505bb7d9bc549c945d76945425459cfa9132577529ca5ce654a4fad68f9af530c52cc486cfeb063d46e7cfa0d7f579498c2c058a7bb957631da53d4568139ca8f7898e9d0773cfbd383bc2642a677c511be3396be53fb0dc98514f79e4384938f53c52c7d4eae31dd0c7c1d580e253d12390ffad2f6b15190be49cdfde95c36ff5f95dfc61095e1ddd6eac47facbac6c24831ff485559e6ba2e7eaad26db67b81456ee1cba82aa56e9da5928a14f11d3e37a3ee3b72daaf72082ab3fd9c5bb9df9545a31d8732d7cea00f1eff81a95346b65eb1ce635b9e4a595b9d1d0e3ab270f1e8f845d3eeffb104f26daee9a35b18be37efa665c5759a516469bbd769f384f0b970c1a41e611babb4ea601443219a7d71da45a282ec14fd4262b745463564b1d56debc154d792f36d84791615db9824ffc278d991b8c9e639d4c2eb4aa3fafb969df96dfaf4d5547e60cc84a8fa6fb2d1a37701918967fa666cf3bf63ee48a9df1858c4e0195e632d8cfddc2d8449629da3a03c76ccce5ecfea5b310f9f809628cf979775dd57c748874df1af9e3837006b6f2a500eecc89add4fef13e7cc000f6f5d134498125a1658dae1e040837ffe47a8c1a079e4eae0f743fcb980c2f62386714f5224f3eb19db7f7cb9a813dfa2e0807ff91a3efe9401c7fad2b6ef9057bcaf67522c1f9c6081c408a4a079ec7faa41f9ccfe366d6f9763edb7a2f6814aea68cb9cfff7f3839e572e368b7ed93e87c05dc927022ecdc351c2c0a059134cc98d1d2675ea8ed86a3a6c548ea110d85ddf928ec2117848a0b1d9f84704fcf88b6ea1883b036d7ab8806df6c5be9214abc9c3561b52012d15dfd14cdd627de9a80d636fe6a3c6f6c8693e75775db8d42f1f35c6e247778b7bd13129b474f324d3a236505882dee2a68d1df9603e34ecc43ef98fcf1cd13900d652406b6e277c9a52beeaa26dd7c48244d36cb9bc6f40de7cb7d5e18fc1bb246aa2f7f8075f0af0782d581cd0a96a07172e7739ff09fa3bc468e8c6bd776d3a8d1db1fed6181a2e01d6738fe5483f9d2b47b1b36f82dbcf8e8fbe5310fa6ff9396d84a7a90833d27ed526c0e05ae1dba5af290674ed71c96cc80af7e80a9c52a943a85ea7e7ef4207c31ecd8bad6dc2b8da67f4e5899ad641ae5e7affe8bf4ace2d2b9cebca8034e49946ce1f653bd23bb50d8bf676eabd5e42d58657e67b5797994dd3f9bc46aa9621eedbed43d24d9f2d5951de18244c089448af7a03b5bffa069ed9ea7349ee606c0cb9b7d00df2ec33e77b42459c34053efc5acd81b1cd13cb4a4d0dc0046c0df8ce8d58c9efa3b5fdef8f8e7c5dfe18d99af3a6ebf1ceb3f21e77ddbb81eb870acb462a5a910e67f02a9ecf871c5cd63bfda90c845bf6d56a84f2fec2c15071eee7f19e36c9491aa88c886160bbdfc99c7e6b6bd8ae13d83e1694cc661542161e65aa92ab2f958f62b3170d7f9582e3dad472beae96f7e1ea7bbef40f7c4d86170418b9bad35dbf9aa0ac1d5c41e431336ee31cc830dbdfd7271769abc142176bc0d7ab7d44ccd457c36a393599daad69ae4d2bd5cacac15794f62928eaddd5189b459100bbadc0baff87e1e06c8edb09c6f9f44fd5197a6384c93c385e5c8d8a0e89f22d8e9457d905e66fbd2cc3b4181c4f65041e76cd791eaebceeaaea36ecc8dc1ac66ecbc9572c4a48b857be4cdd72b6257281785fefd49ddc3209d638118084491670887eb5706a64dcdbbb481640792150a3b3a32c6e6ccd84d2a744e8267db389363e3686017bfb7d0354d6c3463b72fdeb5c78de876b0c3bef8bdbab393dbdddecd98df99b22dafa9bb80118431468e904f44fc1eac9897e84286e3d06e8b16ef542181fb7ff9107fd3d3a371b0ac22d4fa2571cc122f5a6e6639e2320a9270b399b8d81167e96ad01b47004cf476772ae1125c1ec94de8a5a52aa5e7d40b9cc88bf2cf7647e6d9e8c2448665a0b9fa2e34d12685dec8a9513c84e817fbb917c22aa3689e4ff5438283bcd6e69fdaec4b04243e465fbc34a01298f309a9695101566d1346baf79f6e1b4dfe001cab7a57450c3de940c7d7c25e1a1058b07f8e82ef4db2861e00fe9d304d4d2d8294c972bd8d88baed3dd9ab9809b4d3a9f1d2e6b6f4a7b7728ff6010ae16d2fdd38471c588db8d89dd64e7dde99a3e871ef0a07e19b8bfc2902da8f814d00bbb7e0341d99bb64178bd9ea4bfb8675b6ec937265d382d6abb53b435cdf97776c371d66204eafe0dbbf070eabd8f9716c1394834240ce50214846b98c2f7f9ed74149661401335450adc8240ae9a06d51164eb3e6dc6550a8c3006035f544ddc9863bf1f35d8b4e643a2c34aa68e401d35691ff840f518b24e9720ae1aa2082b4e89939895466896c8d48fb2057368ded8f338c8938803d47bacf4a96d10638c9754858cf53e08036ddf10cd6301ffa4f6a56acdbd80db393be0c1ecdbf48ddb84b44599ed6d76c0686bce2ff2e4e19d55aee4d19700757ecf1128ccad99ad21433bf39ff3e287fab5f65e656b0ccc36f40be4bdb87e2b14d2b81ad6f2bb67efb314c647133b3b85d05e7be88017dbfc64786eb3e510e8d06dda6aa55389378e25a55c801fb57043e2c463462371a0f95f2c8ea58093d9dea2ad56fcdbaa6bc78ca79f6f03876a8490e80b98a62de8a86d62b99a51551b418a166f6a44b56cf80cded7034a54015aecb48abddaf23ab5368cc427bf5a2ea79d1588fc8e2662bd63cb9d163b65490d35b4e96d72f9e52cd46c88ea71dbde6557d5182ac8a5103a55222f8ecb1b9df574db97b830dca470837bd7238c0115f11acb4e3bac4826a116e8fd33b4dbbe0f93ff6c5369576786f1b3bf229ff8de1a20fe67cc47fac71379cc1727af749016d4bfa6af2ef6eb9d5ec302d6ab3d6dedec09d55c248feccb5d7d21db5ad38d890e97d071a5e464992f360eb00a81a856829ab267ed4877b993c2247cefb7d943f525a876767126d3c5b4c2f089051646a17dbfdf1f72e666f42c93affac68a76a3fdff88e498ed0e7de372ff01ca01dc34005eefdfdc11ffec52001e0700be3595b309758620220092fa38fac07f327bc82819977b3355b0221a4182a35bb8b5a45b462a82e4f3c52194788bad5f6b2e3702a61992f79dbda4ac32f4b0f794e65ffb2ece508fece2cdb2bb82058cb094125a5a7649ae0c630fdd9137dd367435620c8edf7a2b9f4fb7af73171d2194caae87c370509767698c3e053cd2158514be4b60986877ca20c6ce9b888104be84b2a07a4b422f0fca505286ccb0f09c849a2568c89c75394990f6a613476038b652244d927bf6f8a57f5420c287b03366f1e5602d444ba7fb910cc466099babbc21cccccac395b7796f6794149aa754d13abf99a5ae36c7d32af073bf416a61219fc261b2ac5844f11663884bc069c4f677765a45ab64c1b03c5019f1208f7875f570cc3c849c0fb00b02e801219da15a2b0a5f7231eb456275d7550250e29a841461c60afc776e43856f3240e482d41e6fa5b3318f61aa8ba7f11b76ea28cd633f2f13af812536af93fdf39789b94a1bbf37afcd98fe02fbfd52502381c20fd554dccec0e765e59ebc26421cb48211fcd84eecea44cad6ccd341cfb1529a5155261a7def1abbf893938ba161b4e7f35258473fcb590c8cfb6c6013ebf2015df9017e673ed93173015d7bfa3c29b0f4998fea19a48ec021ec6a62e27a41476c48603e3e953c8cb3909ca1117db7884672757535fde8900db4ccd2487b61765a964e8f2dd09877e9de326b779e75b29859b64c49a7676dcafa3cb306bbb8cb652c52e45b5660389af1d1556028cd3f3bcd76edcf3c3631b27459915d60880efe2f7e69c47e914bfe3a4a8bfb19682b8ed866eefde2401c85210148c66506cf281664cc30562215d3c86d35842bfeb8d0b7a74691d3f5176a0a54a3de9d0faf08775de22d6b24ad39e3698afb2130396ae22c1b2bc1df855c866c56659dcc63c43dc9b76ea5a1967eb945be157dc4ff23b3e858011f6493769e5da8d5d0baafa4848ab846770de678fe00299492fbc8aa157389f4863ee9477632a4ab4a41c7a995365784c2ed258a71f0b7e7744068a31f6cb43607d980d59a3407657b5100ac008fad2a5f397f420f4e7c3ba95cfb842edcc6cee7f32afd2468140ddf7c3d9595ccd08c510abb1556ed6c63e5126e1566a583095b4c24cb88c1c8715af2fa870cfea9113e3a951df1ef1ab59d86212aa335f8c472941eb75e0e187071460e60b93296593782d4b6b4fa32d883ec957bdf7a0244d414468d1ebbd62534c8c89c3b259034708b1551a751d74adb43c2e17ef47161659b0def9e76a5bc165014586159230e7f8e352cf605354ad5e429b048c785bf0c6abe969ca96697ff8df2e7495c7ba130e202023b1fa4feb63f4d000f302695162c6960c42c14b62ae29cdd0d647f95d777eb9c6799cbb8e8e0c571c949c23d16b19d7d63999868c62ec89643fef4b674fe022f00f7990a529e8fbc37e32492389c2737d5932653be9f5bd607b9da75db82ca4259901affa462a838d713544dc6b9d0fbf74c7b370ee74e73fcbc09455c4a91b97767ac8cd1ea6e4c5979fbdfc0d3fafe1bc366c08867bcfa74b3782305a032425b5d5fc4ba15a00af6dd36e78813a6c0b9c58ce50e11a8cd7d53b0dfd1bbb2a6701bf7010a9c341926c48557023b947b04112cc9995f7be990cd49b1a1bd62b8e2acb9aa6855a9a6aca1d37da801aa94fbc525a35d2c5e0b860544d9cc417a07c118945ed84cd6fa5cd68a1ac285c24f884b3851417c69581bfa3c2ab7104f64162fd9b6643ae6b8e0fd9dc5ebcbd5812dafb510149124842db458348f580ceef54ef140827582c50b92806c23d86c871b7f70f6996f756585e7a7df8e8acdb20444cce9c21d723f3b6bd5b3dd35b11cf2a444d1aad3021f1b401b1f4b136e52cced74f3bfdcdaf8b825f4e5dfe85b445dff8ab7dd4b289304fe57ccb4023e0ecbf873983041a67c41ba8e5aed441460d9d220192584b658811d3b2e0eca418987ad42dd7aec85be9aefdc342957f819d2e333d7742f9973b41d7083402a0cf80efb84ff2a0b6b99cd74d4b3f2cd94228a42fbd52ec69817c66cf5ccd2a314e02618dc52cf70eac6af449aacbf197de2fc3bb55cda7af0fa9f825d047a57dc9ef28225f1be8524d101e3e4091aeed48b83afced8f312d130e202e51ee78386bede6301ea64301ea19c9ad4e845e78e9f6916f704f7ed80b5baba83c1e1ab6cb60c1e3a13ea0c0db6fc0ffa65e1c64df4250580e7356ec726524aa9dcc3995a6f39a17b733907cf6ab0c1dece2cec2a89d5cf9d79671edb696f67ae6aab59e8bf9580b7bafed9d48b77da9708e1bf659202e85ac7f3c5484775b2b48b8a3fd1ccd8cb78d3aefa0270da13a0e036ece2cf678f92d5fef1b60149514a665f755c23a471733cf39c14efdbeb7ba761bead4b8d5ac3397de824f24498e07e34a9c47ea52bb2d23c7fc9de3fb69fe274b12136a0df9ca26f6b8e7548c8cc6fed382a385b17c8c527aa09f448099c2ec0f9bbe2ba3cdbed4a3b97476d585c1766b933e973072089d49b666fa6b9de2b21cbb7312c7d4ffa0c51b4c74269863906e893ceeff3f7882c45a8206d0e466c174ec10b5a06d9c78c296809e5684efbae6c506dd73e0c7c96eee4451370eba93f2bdf49c38ef4b6d1b7c30e27bf5f66717c1dea2d3b35035537cd6973e77fbd0184a1e20b88c91bc57a72d891e5afa81b027d57f89a51608fe55719ccd93ea1443518566f45e5b187f6090c051682bbda5b6afb134aed3d0e745e54b9ea8a85d5850b212700ee0c39b05a47d8c620550240ccfeca6f088bcc397ca2b80ce7c375bfb67259d0180188ce28d45d317f7eceb7356773aaebeb4558c0fd0bddd3921b4f7d16ff4b7d515bf04f2fed4ef48d6ce60d3a176b6d8b971a08ede6afcb1c7e9122382afc7c97d3f2d94c6d4bc0e58f5ffd039f7aac74301a2db66f9af9156e4ff6fb804914982d20157d3c10001313c89b37d5daf0ec1dc900f1f26f0aafae2c0aea77e0ba97e2376f0f51b47a46ac8336713b078c9abc97a78262928fd35dc2fbc83c1ec7e7e39301eb9b6a047aae3ab6093bffe58c218065deaeff3faea82b6ff72c10c44628a1e39cc7394d539064103f6a1bccd40cb29c2fa819e8bcfd38833dcde0d1ce5c381fb417e1b854de0ec7c7579803f6aaf538ec57dfd45b2b8eda7e0da927bac9c474ee2090110e28e7f2df7c436b77d290ff9e67ba8bd6996a5b2a70e4f47d238b7bb4c3fa727ae1ef78dbef6a6d2f3faa5a478dfa220e17d78f1d14bfb25a7cc2c69462f6ec80883d68008a0a86d705f715cc564b43430f40aa85b1659fac43a3e457ba0f5279419f13d4d16682b13236c44e501a19d04ad32de06f9a8bf38322c0cf014e3d1e7341272261375397f0f5ce21e53dd252b3946667c8f3ed066fa0edc24d9df3b8d7e463ebc87f214d8505e89f02bb05c1a48b2fc0635f0060f0358429168c05cce37fff4baa048e6c5035c05b422457d3dbc0803c3f98472861ed5c834716f62a2a10c802c8425dffe0360f3e1e44b11406840cd489cd5c3768a16c6e1a69b30ec72a4db47b49a5dd2b52290aa0e6b874ddc958be300abc4820ce422f1afe74d9bc129759f4bc3df2a68819b4498707bcfc8cb920c0d95ecea43a8b331d36214b634549f151a6e40b80dd6429e87fec308abbdcd3e72326938d9bf36253d8fa422ee7ec603fceb29b15f05eb8a220a06117878f37e38066f2c989c974946343e078c063f4431452a363f315a239ddfb6e2b21d9b5d988824d748c3b2623c599b00d32b9a8c4f6bc19a360abe2bdb4f1c5a079d68c821b278fcc08f6df76519f13778d91f6ff1dd02de2a6e5c415d8a187f2fe8212599ed72ddc41d3097a4b54108fafb50550c6e5f3f4c703384316303889944dd02a7aa76cb05b46d598cf5f55c33ea0883004eb761a931c840b7b5f66d9259bd17a2861a34702327eafd89e70d1a40fc50bef5e08eb323f769db9dd624c595c9c1dbffd881b8abfbcb85442cf7a3db76fb9e677f5a76399cd7d7b1f318ee236da0f4f7351a314ad323a5d68cb583a4989dc99d3de3d16bb935d391778776f9114bdaee595c046e0fbfc2643c5193503657be7d52e5b631ad9027cbc701ceddc9b884fd447a43d05fe6443381c89b13e7475cfc3990afacff77100ef573168ad126b5410aafa46b88af57dafde84db836d0f8d8bf2960119a419ca632b76f5aacdfb8aef0d1781437faef4d5f8e29d483fbc5e009d0c50f44561b911df39cb07165109d409ab5dbef9400ca259f8344280464c389d9b7818974b913dba864bea23adc0f9b4f5db9798774738929b1c813bbe18292ece22d893ffb278f165586d9630fa86e17c7e39f5828b20788b9abaafe8eb4747211eb0541feb97b0b60276be9fc4e5fccbb3e3f814a4c9cedae0bd114ad67b42245d62fbd15a5a88d940ef2fb31be327658cdd217b795d245dd61f2bb6a8db3550e1d97b5ec1d97b7759e07fc7da0d8023dea98164d3d0bf82531588535589c688d2ead6021e75ebea504ded559f50bb0b98f0756a9ee3ab5e37f1aba514424ac17b9c279723ce473e07343afef6bb382646b1fef60cdf28602a5242ebe931b38b713b00767e8277276d7721d2cef97d94871f632c61c2cd7f9e14df872d5ac87d1ba908a36dc9dd3edbd5c3d8bd361eec5645840222d3efdcde39305d38a4f5ea552f108707ae722683f58cdd6a86f627349e9135b0aebadc7073ac0e794d39cb93a0270435f82d8046e15d7891395263003fbcff2a1ef9898eaf2d91d693b03ff17fe78a3900570d64af8dadf87f43b32c40e738232a4cac40830d215da31659f59b6b57270ab3048f0e1f17df5a2d6d3237822cc3780dad4b77f50dd6e3649937a6c1a7282ca4a9d48fb2d6a1ec518adb4b2c6ea08da5765911f0a70eb73313155f91f1914c3f51ee74de173f0adee6794b6503be78f472b3f7f5cd2617d207df96ea8a77ead8357a0f43b1d42eb7421ea65fef347250e2a5522a8ef904eb1358eefa7475f89403bf364f7f399a003b87eb7fbef972184fe9c56151dd3fa47850e7e6c4d4d8a95326389d6b0cd8f338cf40d9cb56e20c9dfbdca7b998728ab7fc4aa0949205c9d5df561669b72716bbc6fa6ebe3c66efdf665546f04a22fc4fc279f3860ea9ff8a00676077d71ca5a97e4b59e787fdcd7ca0ba47f899cbfb602c6bf1f9adb524ff7c192a33c2a5624a6295da019d9138053c7a9d735fbbcb029570bfd0efd1f7557da902de85fe3fae24d5b8e15fb0c9ef913bf6ed9adc43b7e74166b5a297341780ed405100b05ec57604e7f1e5ec344cea9ff039e98470f49e09badd2479cf997752bdbb7c94609132c280f4588f173326d9729f8f7a0c391b5ffbcadb40935b12e52c2fa0bd234b165d92fb159802b27076a0efa5d4e1978406b170d1f27566dce6e29d948ba43c6f66dad4ff963a85ed2d99edeb76851e463afc21aa14b8b71c0c199a671a7f4695d67f09afe857ffc4d1e7110bed0fde404fc232c44d84ef7dec2df59c5caba18c4b96717f471bae8a5761d36bcbcfdb8e4f807dcb925b34c65c6830504bf0b91085cac3785fb22f9fabb58d41aaaa222eb276b07efac4260006d0c1e345811b13fe71663c10f47a5fc4f4802c9184fd047f36b1fd3ef59d2cd6e21519b154e0fde13ea5c2899144e2736f81e6722ed6db13ef23edeb25deb4cb643138fef6e430191c9d5845fc7e04a94534fefb452b51a2fcbfa26d4545ff8c787385582f820ef733b01513b44b33527c6d88b2ba9fbe86d3a7c168c0ea218cf63354ea779ac3164a99af9b176442b11ac3fb7d0d20cc087dbb471e4998862ee8dccded2d251be069329d92a018f3fbdb093c731ae19493bded39ec30b39131572c98bddbd9868da30a6415ff9f9b2cc8f6852b9b30d46122b35ae12cafd80cfb2b67e877c94effd70cec2ceecfc9eb9198bd3163ecdc23852d1c056cb6464459f1bf2e712546d27707b22b2f0d8963c88a1bebc54a76b34cf0f33db4309f28031bd7247c94c82e6d9649e02d2d62d51fdd56ad228293f486b728ffad49d5353275fdb3e4433232732c66dccfc0153ee0abf0f3820d5cfc2050c4d53f23a70c289980b15307c64c019aaf5dfc1b7e509463aa74d859890aee265a2b781ea9e471a715a1ca3f8333134b469fcef643f30e481c262ed888fb148f8148d86c796695faf5db0cbfa1f9c11c655c2625f4c344dc02c8347e6759e19e242c0f388e32999fbda77a10abc03556e79e1236211e82114850409389fcde7163e84d97400278fa8a42949f24d287f2e36a1d819de0abc4101b7fda160be1bd40ca968046a4bd10a57430345a32f2ef43add2440b082ed9b4c584905b40a9ff33657ac046bb5f0fae58fce7aa14b8f55da035a540f9a30e3c5662d5ad95da0d62085016bcc4c64d8755872aeb415744c05883aef5a5384c8b824986e707da4fbf86995f2c4e38dd32fe70a649fd617f247c6498ac50f303cc6edffef6dbd2ec8bfffd1d0aaf4deaae67494ff0b643bb4d96f63defbce3c7caf55f854a5e391cb4a00c857c958e5358c98c5a40f66f8d7d65fdc075b6fdfa0cfd888f0ee02c319480751382b813515c4549dd8530ef928f8d7db586ae744b3d699733ec7bad06aabe11898138a1226efc14c2cdbb955870c03d289a93f1df7fef4107716bdb3c461bddf5571b0816fb529cf313f0f7377acaeea630fde2a40f5215459e70fb817032290430ebbb53a4c050fa7b236d908e0466425de4f53f9bc0c5ce07377b9584eec40d4d634df4f3877c9fcd60b867ba55c45158c9acf5a497394a627087b4293f245db125b80dbd0fb92fab01cb1f27e13b276f94cff9642bfb779e8f416899adfa6799bd38e2fad5425fe392c2e28469c3522ad9173bfa8c37ea3e972c0f6a068a37dfa15df2a736f8ac324a97b8131f4f06c27a25991a539e7f997ceef35b3929ffe621ca3af53f86b8490060e8d9dccc8d3b55ffe57e7cdc5b595d9465aff6742446caf9ef1e7436ddb56884103423d244d28a324e8fc101945a35f9b8069dcbeb0262eba362167a0a8d0219e105a2051b655289a5bad16a44cf8542d1822bb6d9af034fdfca6fd25ef66faa6d211aade8f5ce9652c3bc4aac950b43b56e0410f2c8bacb2c432481bea323539ed40817a1772335cf26ed6430b8d936916fcb8189ed51557498b3c8fd81b9c374deb82373d3bc5bd0ed434d6716cb3b82feebe8a8e7a08fba504f0736b75331ffc6582c8628271d2c577e4bccaff1ffd331278cfa37632da2c324060c51a0e668d0e1c989a84d4580fc11c36ca6b7d64f31c1029e1695c4f19fe7d86b3cae18c3e01591e817a178258bfc8f84c991dd45c4e333d1c335f80028738040196e9408a14edd83b64dd7e21b0f0a528fc30fef8485f6362f16fa0cc03943cccb37190a34534f92be65cd07bf7ecfb64a567401bf849587fd7073ff08fd296ac037c892808be5ae54c611403ef39cfe69c7f9158b85ebf766f334a20f5bdde5e44ce531b675f755b45ea09c626054047b7bf347f855de9cd3e0bf27107f5be905b740afe5e645d611f5efd3fa70dcfe04d39605c5ea71e56394f12f7cd159e31e8db3e0cb0fe342a3537fc3187555c4febdef95eb10ea3a95e726d2995b1efb60284f4ae564a823dfcc2c1de54bd3436f5a959eb2f5ad4f29dedc06424269ac80455ea33e84b2e58f43775018ba9583880325e4dcd36ac880e57bf469a71f568add9e1a73d162ea33ba306df86f0391a26e5921332273c8ed231599e9f3af7f7ec3fcac5ae28330cfeb11dc9e4f02a1510974cd0db44fa56e93ce5374578c51b41981f0ffc96a15dccf8aabd3ae3c3b71a0a532c0c5dd945b94dce077b7d3314fd0f718569d99897a66df40ddeca54322368c25c0d9caab652ca360b8db2eb23714d57552b3ebf3937156eba89aab0225e72a23a70685dae3ce75662055aa0341ef5ec0a716a55fdb287b435018450b7cdc4283276951bdbbd38a89e8e3f78337b359cabd5da21161718e55c9dbc05cef6517e3f50c283b9f1f3bb05ec30e3eb5db3bc3ee7ee16f9fa0bc8cd67b1bf467a2a662680d3e16236f10d070193708fbefa0a210f224b034fc7babe4ef4ce7c12178f271aba6b4d0d496b721fd9a2b21e5318b5274f65d10a73b0adc27e9f5d0beb70c296e5c31174fb3463f3f483aa8be097cdb333f3744e6eedf4210b1e3478e3722121a0cda75613e28e9e433a938e3c8293fcf4269d7d616c686b7df755c5f1fc33c3c3c8a5adbde74e14459950dafba419fb3200bae3ff677397a85489e488c76ed383bf87da17d5c292e435ca8394eaf2bbb46724a79db029ad924fd270497a8295bca2df354fe55bcc2201c9fe9bd44fb512edccfc0887c5fa42b95fa50f0285275d25bdee3289c8f4aa7594ef2224236278d9d1a98175ce8b73c6bbdba056576511a0cfd426e0981d4af6133f8d07d73e2370a3deb63fbdb339151e7790acc1ff56864a2e409d75948189b51fab56425dc8dcab7959175d9646b1a5f8fd7f93dd4bcc374decedebe6087b0f07e5a5ff818f8f9e1bb6c6a7c7ed471b8e9c8d870a86ef376e3f7ddbb69ec9f1a5fb1948b2ce31294675660b2955794c28e78469303320a70359ac8ff8c7b2bc0c5d0e699cb29e06f2f1802f3a6c67c737d6bd35b0bdf7bfc1073e92ba60b20813b931ef56f40f0de94937baccf581ebf428d9dcd8f20d4cbafce10a9c038d318ab21a24b9fbce3cc9e784491fe4e9bfa2a59df4be4b853b7bf087a20f1d96c40c2a38434b2753454de0329151f7a3861fee084d535812edc2a60d69f22743dacf11e79937bda876f21ede0eda7e2da52c431990d79a3aa383404c36b8b234cd380a4d1774dc09c92a6a5373f26fdff459b296314d9d588b78f934b613b33e37898e4d89b64706cade0bcaa7b3b7dbda53577e281acc9e686735f933ce51e85ab76305778c1ce3d0056d3f7aee4d804cbab2cbeeb2e2ddee5b1f857a286fe255598f940623c278ca461ac461bbc3794768952e970b069dbb22bc719add1c44ea0e121c0c020b43db5cd3e56ef36aec9ad39e82c9fd278ffd2e6993ba1ce8238165855ee7f9845d01c94d555a73d206665cd705dfbf26eb43e55419990f676c6fabac2e8be28dc53167250fdcb81e11a199e1eb68f424edffe028f017a8c237fec600dc922e788c76fa719ed19663044d36899a30d361a4458ebcf921453a539a45657ee0e9ecbfd4412b291b861e89938e6c18a575a7058bfe8088c7bf0de9fb1f9662596b17293d72797a4ad4342b0d2648fdfbca9dd13839593de157670787c2ccfba97e3be86ec314e5fb07200ead3bb9fc41596b578a6f178508b404badec53fc2a03cd36219a3458096b55da1a3b380c832ed3f0058eedc552ad5c26da52803faff26fca954745209e39c25276fe9f1dee71d0ebc6f7925d47f9fef64176bbfdb3c2f782333fc6e5832c94e2b5a2a7359372120d3f9d1a251241bf12045ea3ab8cd0b134e17e977c7b9bd7596dfeb92e7faa0f26a373b1cff314435829af572ead2e3b55f51e456ab2a4e0e755bc1c510a885e000ca9e5a3c01177b3a83e68382ef1def02382ee911ae186a3a76f3f1b06a58b92ff65cbf81d4a996aa28fb917e1feb51901b3b32befee97ba804f407ecae87eabe44c29fc9fa399ca827cbbb770cf774a2b6c2fa56323113552f4ef44a84f587ef7f02d7ec41867713b26433afd312ec6575c1b9d2d4c0623b32a62361cb7787db80fe151bb542097152d738b651cfc3abfafb5f7347ce769683fe795ddea0053f00f8d6a95b94269de2dd957466ee9f57da04fa7ee7ec556c60a3ee5805ede6e20498147cf0488381d6fd9c887032e22affaa6f23032bb0eeaa7931ddeb3b5920eb1cfc29720d28df0a90471e8bbd962e5ffdcf5da257d5486f7fdca458a0383bd0af0b29d6a16fc8a4c14854f7c1255859aaa96c433b06483aabb72ab56fa90ff6acb7fb6438a9484cc0f19d93d5b1fb6bf96cf73ba203fcd3d8f1d38667185cfeaacd9b24bd2649fd0df93bce2df843e999f4d1f55462ce9b1ce324890c299136399d4a398b02f66b79e22bd7ccd811216c849af62640b8beea87e43b56d16f5525336a02c8d8ee5cfab059754504bb25d4c1ee56bdc224868d97b6257b313d8d0ae4ac9ff9f24ca87205e5da99cd2ece0a82062182814cbf56f0f6854da633f56dce5feffb7005ec55a2a035511361d8d461ff924593fc2a90f7dd4d5bcba0b44cc967c0618d8bbdd372068b0d0c2a02f73843282832bab49e0c06b11d66ebf79cfcd4bcad4430bff4ba2186f886c639d2f0c98f3fef81b7eea9209ab803b6ec88112ca23d7063168f449acd0fd9de05806788f1dbe7d21586d16e3ba84d21dbad27ac8aa819a20729d77e2862970e11846f2b0021b3d88f785c6f51bfdf7fb5510afaea6ee5a774a541054b80564574f4f4f0a41957a1c3144b3eb0efada15966cf8151a6e5b539fdca18395d31defb10fffc8e60b3c9b8f32eefe20b72ecb160d9764edf77b7250fb3a5f2dd2608faf70ffa97d6c4a88f189c8d14ed1bd23e638ea7234e6193216b3d2f6e5d737367be8d45541e263239523d7bc64d471e29bfb2e51399a4edfa4e53c425d2841897ce384f3caa5580b4cb36c49506f4bee04d78ce37f8628f66f890cae9ca6d832d893008ba496fbee4e352c36e62ff6bf063676d822919c9b52298c8f0aeca520fdeab47ba9ff91c6b13fa3d3afee7d3b63a187be0a4e4a7775f83d339e606b2309d25ccf0d9571957fedca3f5f94764996c3ea36c9fa7fecbcf5535c4ff72e8abb24b87bd0e010dc83bb5b183c4020b80e160204824b8204777787c183bbfbe032d86083cbadbc9f7cdef33d75eafe01b7eef9a9ab76cdacddbb9fb59e7ed6ea55bdb26d620e024e1c09357eca046d532f30dfa450b175728d0283ba65232ee5f32626f726f55621848b272aebe02426ff2725ce2772859a05ea19da87d4f8bf4cc14aa0a02141e8e4a6c64328d882305756d91c6b9aefb1aac7e24e989f64fdfb4497912f444b742ca0b678d01f695198d82a9998b21ac6f36d6e1bec2de649d8fac69cefbee8d5e514e78c7e20f9a5599e22ba4f2a658cf2baa6b5b47c234ee3f5c45dc75425cca21933ee799ce8cb03e0aa521675100a5c6873e73d3a23e5d048318fe2dfa386fd74fcb57fd0b60edee47cd50ac8ffae8a6b23fcfa6ae14177a1f0a8af6719c330eeff65ba1c938fbaa1758b751faad376a74e4c1ac352df90b3fd640b0aa34c41edc06e429ece409947eef822524f2d486d9968f3c45240a4073cc7ceef3eb04698935feb2b10b82894cf616e27bea84583e052614e2b333df53adde27d3dffa415d3c9941ee673ab657585f59310659b1b6e371df10086a2356ed99bd63183d255def6fd472b449533e16f0aac5d29f9f59ee505791052847044504c05833e7440db56b150b35c4142c7ec0ad1a87890dfe66cb570cdf2a3cade1327d2a8c3d8355f97d128e1e797284ee996bd16a30e2715047e2dbb41318e296e724d8462ce0ad5936400645567aed5050dd3c934edcbdfe312d49bcedac61301476ee5549ffd2503be46e2843d1df458e60b219c723baf5bddb73d793eb9877a5ef07db3ae6aab124c72a2439eb45ff737fea71a4143289881c1509c357e23669381b1aa032f7a4838af7b978cbde03de41f5670a0ab5875d2d322aec86c71f989abb2084bda6c90389cdc1bb3189faeb736ac92bafe597a062ba02d5a51bf3b8529babf2e720b65ba42d649f8b321a7bf321b289d7c306d3c765e8929aba23231e0f96e9fd4bc8d72c0786d9e94fa91ec66d835083e3779d3b716ae6dcff9404e0bae1d1732753e65ce0963d43a642c0fb3f84744d3109e2dd80fffa32acc91bfb89b65e0b187dd2affcebd8ccb8573322f9bee6aca387cf59653aac27cdeeeabcc95165883c3c77b95edb74ee9006255cf9d4226ecae4b205e7aa90b88799c086cbc47d1d20d9db0f43a4d57ecf806baed96429ea60e7cfc8eef535aa6ff20d2b1dd9669bfd74b8c7df4f8533250d58cfeacef9274bc52783b7c6d559967ebce94faba7615d4ae61c03178f977bfd22e80ebf87b2586419924ac344bcc97087a2ea383f4414a4f6215cd8c89a29fab923eb0680683d3a13c753c82e5444fdca587bce21a0eea571e71e9a49d31310fec08aadf00be1d402b1ee9adfc5c66e7b891a547712b0ebed2a5e5f49ee194f0f6b9579b439cb6f32a5b59bf5b79d956ff84b77e4a0e68a98ea34dc92839a8c27a39b6aa7db90783bd8d89792de7a6ac889a4ede682c34e386ab43c3da3734ab9c03d227d3fba82edefec34766da8407fec18e87046e803f915486f1d67b2ead3b0cfeea5e47535cb4a7ae3c8d4df916d3c77b39c7c0b11969ba17157b83a8b521b27d366fcbf639e297bf5e15f81b13e54b2b009532ff329b9e26837fc624b5d9bd6dd67fcf04db0722739fa8de9fa8d76624a0ae5bd5faedf6b283bb918f859716c1165781d3abba928f44209503fcbeb5d6624845414229e3adf2ffb18f3a20a3f9db99fbf3bc14d832c331992308fcc22f5b431096856f2cd5459829518ff29afdb61de489a531fdd8b9ccfe19c15d30239a7f688411cd8b037c50d1fab1551a10ae5a45cba923161820c4f1b403801d6e26419ff5f95639b989f572d3dd644afac5c9b09fb2df54128c279dcfcd990af625766c3fe6255b7d8851e72cbcf8cb327118b7bebcd6ff2c5a01027b7e997b20cf83ae392bce67271c6e629030d6fb369426202c69dbe9dd18e646ef58e53b2bc11979adf2c2dc02ea979a84cf0ec239b4e289dafe7c2b99f371c44233d890b9b48eb61dc50114f21a48d6b37b08a81e70ce2c3cd5d1f8340e7d1c2fdbba8cbde5091efa64199a76e7775bc0e5ed38797c7cbc009b85bf225af432b06cb700f57a505ce39db866fd3b418a0a49e06cec3c93b27690768524ac98bea73b19a783d9e9421b414728055e2fa7515c8aa2ad828feaf2ac2139ef70d3c7d35f9789aeff28aeccba220c7bd71fef41c3db23d7cdd1c0b4655a9957c4e0759ada206685c1ee6ec6ef76db48421d35aaebe906ff84b0884fe401cc1f29ea189002c59d2a650d97220782606226c3214cfe03db9ae01c129fc3b9579446fa50ff764183ea34a67f5586de1f95f130bfff5b3b28912d5bd738e0eb4ff54fa3ef5ee1d4915bf4ca52bd664fe97f5051c032a8e48910c2df6ca4e5e279d3797b60588afff3f40d2966656cfde8a65f07c811e8df3c75387866c0db0e9c419c74827d9c2ef03e379d12f5dc65b9490f0725a3601992573ead75c7e87908b119ac6f2a165ce7b79d2db6153cc7ac71b015feb3560e9508e887690fe0bd51ad20915c4958a91bc8f364793dd1fef14da250b592b4570ad0c4bde2fe6be0ea17c72f5e25d2a6e190d36e908539c5d1e78fd40178cbbbd64ef7f9d59cf4a6e1aad484360f603827e19b388c39fd81c226f05522fb906d62bbc9fa88fff9569fd81acf03e668dc516cbf1b651461401e17a51c3efda73453a6d5ddd6430f659e466b7bbccda72aaa47fe9f3c227f5582ce9f0028059fa63bb03abb22231d9c000f3f89e90a8ea86f90a01a7df72f017cecf9b0f848435f40e85b49062096b32a73ff22c44fd1745abbf948cdfeadcd2ca7cf536d397ad19dca19090fcae1c2fdf358d5c972a64ce0e95cc69ca9a2db922109b44aedca1cbb7c8767b5a8c20128973b2b453c8bd0d507099b4346e200c9d6e5ec82d74e0cbb200045dc38fb3fd3a22154d09858ffcf6ea11574a8c9f9530ef8f500d5dcfd11922d8c841332b24d61579b3e8f5d988f748cbdc9f33ebaeb42e7bab0f95a33a41deee8f535de1b23cd5d8dc5483d19775a050703a858e80f048b993e08780b5ebe89280da0422012fb20a508576f312c911b99906c41376a5edf3b13d3bfbdb6a50b35aa8c50ae2af85db4e0fe92720ef1e7e7f97b76f70501fdf0cef78f7b316966b7043cdd4bcc217052530a0156bc4fc9e58c9f89d2576271ba565c258123070c4850f1d1fd2ca7609363058963d32c35f99966a144d84c1b2a85792bc7d0c14f005888c459537341a5736ed11db7e5f89e0a864aede75c8a3b4a6d73dcb19182dbd478d3888bdbec949d6ff926ad323b90de6e4cd509a84b17fbd7c575839060a5efa9b48374cc2a10e6cac6e395309ceed0b59bb31b269b3757cc3810509711fb823c5c87c23d0629e982c34c8ac5c9c64c3f8b20fb30b137362d31fcaae837e77c3be4cd91ab42342ce463e352b4cee224fc2e79bb3fadfb20f8d710d5cc94c3b846183be8301ed0b46f975f36cc9a5d24efff533835f93ffb22448f46eac9bdf345e84167c300036df2c1e530a2d33140130796dcc316837e208a9621fa52a3464831835d646f79e292e970f82908d256b7b4217c254c3581cb9a1f2b5747647bb40cf1e8ee25cb1a478c71a83fcedc8797bf285e3851f39e113136497f08c5240e30b5ef0c0c81ad37208ca708fb56e8fb8f11bee777ff5d036d91c835222af827c4d27990f166d597c8336754911d65830e155117edaace92db370d00b07bf4ad66e27915d471e4ed52de854f9ccb105db8cc5656ae41fb256229502f40981a680d65e7ff6e4dea11c9bce653a64e27563407183c086b77066c82be75c4791535cac665bea8ab5880b13cf9d6c33e516f73b3755180937a01380b82c72da6ba4c97ffb82cb25a1602ccfefa6a4cd426fd8f9b686503e7d8ead58bc650e58dc690395bb61b654fcaabd4fc84d3728d23e4dbe3da3656472b51de8fda625f4f04efb981200048df58fd91b77ecd6a8cfa0d768aab4115cc30b99a98a61dc1b7c1b36d40566160f9360f63df2709577d0deb815968ae4c0d4d05d92d86dfa417702f9070adef27b48aaaf76f5d53f0e48fd0ba5eff921ec8eaac820c7775a8a99f180f3a9c18317fd61460ca2c80132e0355a360919b9d3a13ff5c9ac1c9ae9b6d5593ebdd2cc3f171193b54bd19ea89b975b77975536be19bc9149ff428820d493189a145adf6183a5fe0a38fe8ccedf1ea02962f2cc61a56c5a959bf736488d669328d09852af1fcfc2b987f112a68488c4103c6c5089b3d9069ce3f0ada3f93a933f1d7263527bdab407276fc883abeeb826b2a62571df07cd75d8fd2e19c3e28a3ea071485d80a89537add0b75dfbbfab5921daa6941a1cb9540105b3d14aa1ee8ca972951842ae6afdb3983338cb8d8e63634699c54232ed5d058717de443a5fd0f1c5af0047f6824f89f9bc3e6ca00bda68095b5a787dcd3a263937739c9d544860c800c3f8a6338dfedba2feed7ac01d65c42c78afa6d1cd705e52383330adb9b5961db33e657878aa8deb162a9f83ac7be2098fbfc6b657e9c8f97d45a8be369dc3361ce1ba08e9815d9f5679715abf6bf026d4c9f46caff727d3bdd9cd5798080e6aa401ac85fca957453d71a99672a88b0f7c876ea3eed3dbd824bfee691397ff3ec86ee5d3b704eb744c63dc37d945dfe5eb58989bff1f0bcc36ed7b858c3fcde040a5cc0fe45cd179cb5652063785fad16105866dde0c85eb6ad1a0bd465f89713fc9197a1e3011fff29461a144886ba7f69ff21b06fea7bc2e23390f5eba1526b170fc35281497c6dd651a0b5fd88ee1a20d846c5dfbaa3b91e514dec102bdb201a552f7d5a2aa2f830c2237e1a46b946190b4c3dea693d24e604ac92e3eebfb059137691da8b007639b3b493ab939fa3f3777693fe1505e55a3452fe3f59ff4310f684c50018c649725f5eaff965409ffd001984f1fcf4a3d685eae11ba47e1bf80b0c87ee34aaea4a429da4e20ca678c7bbfe913c7d5f08c26c5918f064c3a92e1bd8612f212242fc62172d5055ad465c06d76e0aa6966b589a18f9a750fcbf13b2169c24d467f60e2b3eec5655ddaea0bf28dc3768e43cd377fb03e7b36e581b150141597733820338b463f6f6ea546e9a6b7b3299f09c53ee4a7d6039cacbf2e9eaa07d65e18cd8355c777ff126a62af8fc57e7701ab8e97bfbeadfc8d60a2a95fcfa977e9d1d9059cb8f0ebc4f469239058c92346b0e2f32ea6650b25752d5963521bfab9245fd2f36dca8a3bf69eb012112d13f1ac31aa50f3dfa88bb9a80b5e83e3901ed39571a2b22299aedeb14f6e5c27b47060abc8dbbc47723c6b061fdb4915d9fcc7f1794a1781ffa675cad44e8ab7b5cd696766d8f10a0ec6ced4a2757853133747ec807c52f18e937a14628ee3758687bdf5fec19abfad0d9199b2503d3db35a93c806580338c15efb92098787593a54e6e12ac1dfa435f617fb12aebdfc37c1d3864bcd9cb9bffb057395c0ea7f79cb24c55895a6bbc7e0689bbb841d4916b093e0a62b2806715e6b2cf56fc05415b87d8229eefe5f43284ff5896093c3c1cb5e5cd3ee69d78e2281a7f55ccf1f0652d0c265ead2f5875060ebb123fd0379687255bffabcbfe47f564452b5b1a48bdc869caa8f7f4a89d77b7d1eb9a870fe5a1a15a939f96f15f0d5f44be147e5d1e7b05977fb74303686f98e1af85e80824116949345dab4d3b6284b483e20110ebc953393cee23c9bb8b32d330f7f57e57c3cc2284bf6f638ecbceec44fd0358114197ee786bc43a9be0ba47b9b02887add395e2769f19b3c06fcc79f9f6368521e8e78f57ad7cdb11690a6da5af1f2e76cf946ea4d2f8aa1c3f1c6937acdd931926fc0b49c81f9f4bfb63bda91c2e13265b75bfbb379a8a9a9938f3286f5921170a823b32c263dc9a99fb99d0bc241f040bbbbfe6898e0346fb9fd32be2cedcdf5d4e8c4897dfe29dad549e040ceead0d1fc878b51b18a01eef2cdc3f6daf91192fabfef74cea605eb6ed653f81f73f2d170641e2f9c995ddd07822f1bbcd21ef8b22b9debb1514cbc10ff1878046cdd0df156c58fda00e4790f74cfb37e1bd707f21acc311d5f5705dd3a4870f1e8020ff16079367d776d1bfe9ad0272290244b34a6987fb1fd8a93cc8e08f1bce1b933ecd6a8b7885e5ed71ba58b2b84056e54b8be8ba8f5ea3d07f5ab8885e31ea77dea495f79e290232d835100ef0c7518dbbebfbd0ebf91952687bcddfc4e893ec2b443fbbe8b68f2b15fe376c7a31188aa9e719dcfe684d240c9d2a60e8f57edf165fa7cbf606b187b7520a9526e764ab99c951c5026cce0bce2da7a55b345385f53459c0dfa928da37fe3bbf57a2b0e85a9b8015b6c75ebda331f2a6bfada96ec7b8dede7f61fe5f4a682389006eb24cfee96495aef4b3ef83ab1b64ba2035c9ff0eb3e7526cdde0108dc46c0dc4f1cba9e36baba5990967d1405e49efcc4d92bea629e211dfb49ed882cbf5ee12e2f600e4ae191663c976f417728746043f8fbd7ffa7c3eb34d5e968b1c6f4fc953677c6871b5f6db1b4d65da9179c13c95ac81942f0ebea31e97581e4509641cdf024f45ec0bcea67db49fe71b7318392a1679dcfdc29478d1459ff978b7118c4b768aef57f6f5df5ea9453ee4e9bc0e3ba8ed3813e12d0fdb24708eb02c46ff696363e3896cba2e40a904efa2e4a1800b56b58c17be125c09127b933e5f197c5591e65d42aff104382bb6c76829f87741bc7482906022ac3664cac86a5508fc487b26eaa22e8884999c3587754b0e4e8ba32f6ea1a045f3f13a25c4f930a7a0d1b48a8f72f07160f6a1417463e28baf024f5a5649627100cbd95862cfda4951f5ff62e9fb210809e6fb883dbc598070938750a1dffebb147a0a0d94be49781ecb593d68cd9815f8c1fa347960c6f731c0d828821b74e072bca732a81cd20e07d16f452840981e528abdffcae5f3379eaa6931188a9d06c9536714904f9191717ce6b26209cd48569e9fbb1f6cc63648a73174f51f476c9db9c8fc3db0dd683b1d5fa775905c23e4730267ed4f82f4fc1563814572fffada3bd9202498aa0f931b55499758ae64a8470c969dafef4acd61485794dc5820abf43af968c3577a75c9e9614e6354d44ad4f4057d95024cdfef97d6b1c031d55860d1fbff9a52fc630a28e0c651d22556dc75392101f598235d3e3d7d2e72bf85af9a2a86cd36f8130a066cfc34118c66d70396d8acc5139ab6d671fca482a945aafc45c221e64fe27709d853d10af2950bf20078a00866bc936035bd0febbeda747e4c12b6dd58e5f45d5c59bc325aa532fcc5add69ccad718f56f5cfe49ff13091434d6f3bd78f7133056e931d0669bec0f30654a011e49f6abebeb59d8617912bcd4fc132c587b153fb27a2b00a6fb7e97cc4bcb831414d5221459e035955f541e04c37f732ab3993fc6eabdf8f613315611317414a0880fbb23c94f5f72bbd35083eb162adb799ce4028d00b0b0890c119b57375b1c9871e3ef1196bd9abcf23ea792bb40e3b1aa80c0d9482e9fbf29da223332deacc0e1c9e8503ec24d2342478d3bd00ea51c7352a260d734e0076cca83ce605f77edf03a9f37f5acd07e28992dcce49015e78f0de8c98d987cf25f89688fc150bcfecb7189f59f4b8c32611dfaeddfba548260c9a6f309d10f2f70bcdf3c56caba1c39db68fd8d6938d5a84527293ae2eb1180c05976c0e9bf88a90421c1b07d65dc844bbadaf32543af80560efc481f4bbd3df45c4ad85ae08e28ab51416e38f90b7030203de4ee724d89471288ec59baf75ff5e210f107ad0bb53d51ad205f0d78d1c327f3f6a713a71b1bb568eeaa0d13f38cd6912e35c07a1ad567622706d55861d67f85cfffd6e22b850ce7093a840217da1cb944a5cdf4ba81d48be88a50df55b6594e754eb1c391881d9fbfe79585cc71d91d2c50ab717ac2e6090203e423959c1e9f19df66cd50f78de7c7bbcb13c41fc5ea8bcac0594dbaa6bf01a8aadd158bb25aafb8ebf79fbe2e9d6cea32b55f9a783879694ab1bb7c07c1c06865f5d8fbf63bc07ef1df7571340db2f417020708d8fd5324ee96743e7d95d241e43ddfd25dffade76ae4d3af5af1fc85470c0844b32ae27ffa5dec3d637b9a50b2ce06957e3670f6287e33e1bada23f04004f6986bbffb6de659c7e2673210115244787a11d26b52aadf6e2af06fdfcb7fbe8deadf58f4cffe92ed674e98225e8d08c9228adc36471d9d78f0eef89dafafc0c4d81e7c7595bf8eea04c4f9fbea8d244285d87b76e564ec3f84ee49501c07885d04e24181033bc310b5d21b10f5a9d6a5a30dc5d2bbff560afe1d9208b852d89cee9a75c68ae67c3bfc3ee80a5cfeef3ff84f4b0161575ecc7720c3d34e627c13d62e2c5df87f11675aa5647fb1d3bd48faa532f2a90b32d2418cf1be017c3b792dc2c2bf39d5ffb0c5d98940385f76e37777bb4b6d1e43f37f4ce7ff0eff7f199e03afc1c88bb547a023d12a5fa7b237428dd6a8a4b8e872216409b585181828ba5f90b08334df44a1ebe894d496f14c2da273735be333af080628343e76d5ecfe66d94746c71a0fd1e28127564ca765912a50e0eb7a15ceadb8f9bef167541493dce254dd03d8773c751dfa72e271dab12f7eef5966bc76023e02b72fb855c14ea00d1db093861b522e2e33b3aeae2f41f08888282858582424fff7c1ffc71ed823216edbef8887ad5070398f596e3c74401f001d1f383a81a280e7ee2957901815bb440b857ac0a9d36374e779e482a82b0ad2fff83bdb2bc47debe275af75ff4087960fa018c5003e324e79fa17e2edd7f8cfb67c2671a9717454aa729874e34315a7691f0102699f417b1e55be6bc47abbc442e7e35e1d97f6fd89e76cc86c3992744b78bf193115f078a7db08e0cff387997e8b885c2e804e3e3bb55939f34c135237bf48a0b302667a290c96a97faf101c8fb1647e6d7e133dc0c7038849cef23d325ebf17b831e5dec954c5a2693215cb8879a6fdddfc862ee716e14b82bd41cc00eeb8d77e2061ea23270b89da02034ac04551e00575497fa448d466129aeff97769b11867637da32edec8f6ace791c03bce177ce5b0b351ee33fbce6b7b27e4894092cf4619aa4a0f4484824c9a89e40486639ff89ea3ab8010e5f5bb515a12200997b3208a38ac445dcc8b272b706edcf6aa60f0193345c4317f37d942b2dd5934d381236d8cb0f2f81eed61dac877c435aadce0db22c698f48fbe0afe626dd6350f225f5eddb5efae51d1bb21a40edcb1a945ede69d29d95879b2d35f89ec8a3adfd62a7f5ee020003dc5976cdc372400c429535e20c91d570513bfc67ba5c57ccce85ee61ec0371d2b5c1d3c9bb169dd38544916e16d54890067ce6d74a667a61fecc2c8ebbfa0a7a7cf4479e0934192af69142b47e74339c28baec219cf8716e29dc1e74e4608f249785f11e14af913ba860ccd427debd3ea3d78a2605a1696c2edf9cec2a33fa9bac65125573e73f47feddcc0027d7fc33af08adc079d8a8986b31979ff3b4b56d4c0fadca94b463ffbfa61d8ed96e5866fe78d6f27c2c767342de7c47c370bdc930f3af48849afd750aa5e1ed703277c9faf5eaeef17243808a2dfc9f711534fe70c8cc404dd5d10de9aa05d840235c11a83aa9a49dfe7cb55b4ad1bb3a2920f87548c6ed6035ff632bcc02ded542b4034a6beae2d4e8efeed153ecb746de54fdfdbd0f93ce5e49306b9f8c8e807b7772fe0c906345ed45f20129f03e6f4fde7ae817e5a68df3c11517e77d9885fa6cea84b3c5d65d1dfd930260e518ac6b77b7809c76410a8668fc9bf79fce4f6f591ec73bc95373f32c92c5fb751383e273e6e63efec51cff27c9c8a3048e5c605bec8ee7d4e5a55bd4c9237c174133002292161b1a35e3e84d5443b67c6a384780af33811d1b0c485ddd3eae27515f7bba86b56ee8a618842e0b03e4f92d0bceb15797adee5bc75f9ee67cf9aafcd42555dfe9d9a922db86ab9bde46f2d7c4630e39bb5b8c0b40215bbe39d02a712d11d989ceb917c08cb1bf937a447494df7444d8dfb11a6b135f05850ac8214c5741506152ca5d71c5c28897885af231c4ea35d2d4f8fda481e909fce154df90449b72270bcd6fd6f168283577aba310c2ccfe8020ec8e075e1b8a67bcf56359d157082bc6d2ded8fe4c73d12b13bd9c5f51f729ead4834fa371e627b7604140908d3571dcb8fa535e911a6790cc1f73b1e0b4f7972c11ee2a613bebaf6f640347d09149d30945d5187f8a8880e2270a9a247694bb1bb7bdb734a2212b6d74ac32dc3c287bacf922dae8629a89864af1fdf8407b266b8c82046563881873f79ec18d2f70c13512f39173717729f0da9075ca8071c26663d16add88c3f0b605d65a5c63055854d38863e6df945ef1665e44793173940a278bed420a558e6caebb0368e7c983144e3f13a6c37e01b9929b4b3f5bd154a76e9418fbbe0a964e94a7123aa0b0c657085d778b68b235f79e7efeecf262f9eae9491c7c239577118e58a5901abdd199c267dc37d1f2a6597af7adf988e675dcc297f8469f9eb039f105bf1f1b968cf8ddfc4cd2386c02b4435867041ec8b37bff27e80c227b9f45235f6fd8189eff12f0c5f47cbdf88c166b59d043f1960231ef1a1a834dd165f1fedd64b34b4b49c1b9b5df7012742c3a684c2df1094bc1a77352b73a5fb14a10aaab35a6f69335835490db631afabef5ed80a66afea4868ea3cf9dc89b20e5ed660f89580624d762045de44d81ff62c6b8ef1be66376faa692c7924d07b3523907b1bdafcd031eb21712523e17e7f3ca3fe4277d5a6fa69f048539ca3760b0519e1fb7b66e1d2dd1f6353feb1e2f777f4bbd3d7d3e5e7774f83fb19195806022165c4882ffbea6bd431c543838a09be2e1621527c38bbc4fb9c59f9cbd5b7df672daec64fd6d6da625bddc48ee7b101d7795cf59d8feb2f0fa64f84ce91f22620ed7dda1ef4e19afe111ec79e5f25745df539a7f0e35e3a23c136d8e8cafc1cf7727ddd4932430d11416154c083890946fcf49a9115f2758f727d9c5d1cee9a929ee854de9eaf98fb842329f21fdecf837a972c442c0fb94bac09d3bad770962a737158da460294e7168380df52141567bc73ce357f151dc9fa8e833f9dba665f6925ef09dbb369148f49a74599b223868db395ebff42a75743a3db1ded78dba08666f1b068b891028a8ef04a157bfbc1c1f0526d42a4ba0798474cb8b11af162c5f075b65ed41820cd3ea73a875a32379854055e5ebe8786af05d0539cd0b4f0f859e67bdd91a0968876a3cb6c611d7488e8405fb19bd189ff46c49921d5fd5241cf4a37257d3a8cc41f30d6dfdfaf5a0f6270c6e8f9497256d84961f938cf5595948e4595de6c8d50f3da87c2fc004d72375829e1c33977d41535d3d2c1722c4be524a91f9bf0cd8831307c821ad16622d33f515d4647805548b1485a89a61d0fc127dd812158ee6da5e9cde3bec4cb44c05d72e09dd55371a4f2bbceda11dbfcce9367208f67207193a78c1fcc3cdbe7c7d69034e9b0808ac954b4d1e954bc52bb683d2b7534e0b6878989d32fa1b7596479cd3efb460cf2d93e0af67431ff6c14db46cacb6308ee14bdd47d177ca3c0ed66dfe489d1dacf9d40943753a4ae3e1ffb6d224ff4cbfc8d5870c5d32b531192dbab2289a7dc3089278869aa1a53e717989baf4bba7425c281c59dfab670944dcb3b7c374c984fb6650b3e667ca4efdde2a29f0dcb24e1823dba46b9bea84fd9147b8c45b87f92bd6041d5b8ce9e3058ecd82cbbc1ff3cb985ed71a26fcd5935d6d034f04a8a5c55cbba547a2c7c9ed38f4fd2c14a9c9de2ebf4c564e247d119295411b4453c3e7bf9c496ca6dfaf69379d3f072f401e4d09bce9b2789017adc9bb7b04afd1caf26087641652f4545bee165b5b23600e55613745805f1984cc8214c258ae53ad8c2ae96b259adb838c9852d7aa691d799163d121d616cfa62a8851c995de1bc0f51d13e4bfbc8267fe8c0079cf013ef1c109f970eaeeaeb69fc7a94ef5c09f359aaeb19cb3f3b9a84129590f678287c2db8b9d834b0a52280f764faf12e4345deebf49423f09a42801e8cb371faa96f45e814665836de71b57b21211dc39eaadf11a38584a0c8d0adc787cf7a18393eb4786029b222621a7c61785ecbff6d6c6cacaf175782e2975db980d3fd86212eb0f270467d5393f3a4026944e8d15aefc4aaa3d94a029a3f76903103e96f0f5b7bc41678d4e7780ece9bf03f75631332bda9a6f65c6bf850284f691f22eb192a38a14a071a4fd3e5f8bcbc2a2b1c2b21872bfa7495da396be249d30a294c231a6d62132ef77dc2f9c045f0019fa44be105787b484df728ccb0c9a96ada1265e00821d104d7f5b758e64ea5f9f4b2cd12cf9bed7ac62c8b86ce3c072b567c77190d0544c6eb828eac3d33632a2cab0f87231663352500c98d90ef6cd563cd6f657bcc5f82fa3ca9aef2ca351ac3ad1d2d5704058774dcf85facd7314d17a8cf31aeebaf115eec8f8d78166dafdeb9831f8c139932bd7b4cceca9c6bacc7ac7f8786420e60740c4d38abe05170a4bc95ac0a6e37cef47002116fdeee887a86b0c788c78289c009b50e5ab2fca5452c1f789089d19f0eda7a923d636c4d233f02f011a125588e6e4b0fe17044a327408e5c8c7b9eab5b3a0ef5275175db950de2a5f920c54402d3ddd8254a35c1b9aee2b22eb5d43d24784bdd9d04c78cd5b81fc813e06cb93d3b51b55d960575bc6126cb0aefd07f355cc9ac115747fdfbb2e68dc84df495b33d1a432acd0fa90edc8c1c1ba5765b97a3fef97e93ba25e2c23331f9489b99939393a148c0556e2ad89353beff1dacd8be5a2016185941dd33171ca4a231ccf9a8214a7b3ce837c05a1bf558af2fc9ff3963e46ebae7aa7afb3b1380cb1c2f8de6a18eb8e32c7433754f16ea3b4f6987868da85464a04bc7ff6ed0f70b50315e8b17bd202bc358feb2efe01bb9630a95e7e2d5943c75c601fcec834d87cfe2cabb14bd8fa221f6888824909bdafe07d29423461fe580c44ed6414bddd2d1b0ed382fb15d60e61a3fad4092aaa4486d619e08484b4c5a51e7db539c026743717965c98d7a60c6b585dcb630a2888085a288ca30cb08cc007ae7ade87ac52280a8212593e6f801db15a65d49dbb2d0e53efad41a76b51edd851f1c659315145c86d87463628dbcab107b5f4fe99f5ab7645efbd1c7747e348b8f404e6d1958b1f6007e0007be9f62cb1d6c5992f65e33fd119972be4e7e7deaefd0cd9b10a74cbf6e9119e0e8e8e8efdbfadc4e66f6d07a52ae26296d5c60c561db6bffa5e94a71cb77dd055a3515434cfb781cc3c5c52af51585cc0c332a467af0e9d5885983911b9d11388c07c484dd8a9f3811ef8c6119957219d2b0feb2ab8078697776f1ea26c08d84aff8abe1a6d67937e963b86cdfcf57951c9ad51d1db5bc77c299928f127ad44464e07291a8fee80e7dfa679cb6759bbae360eadb2e0129ca0f794a86432643a442e844b9f1497af979eef4f1d371d26425f28a12e22b26cfe745c49113fcb7599714bb17c84317bb8567cf979937a69ef2e275743f6a00a64fc5099b7efce63454b041ae9e9800cfb2be08d31df42b4bef6212a6d4b345db3b2a5143a8f83120d5a19789fda3b4311ff47ec35002cb324fd36776c07b5659ad86314edfb6a73381e5355da75215c39606eee5ed73d09ddf7e15d5bcfd4ccbd8a557ca807a6fbbcf12ee9125f3c3564f97d3cbb5d38b3f40baa36ce671958265b1cc0a8c448b4cd2dde7ac4b8cb9d8ea08f66a8cc3158136004e6a311da4253878be6b9f585fdb2bac7f40452c52ee245b80a48809966f633c4cf067dada30915de49f4c2ee1c0f1e89941eeaf4bc962eb5483647c5cc855be6ada1b132714547c763507328e89fd0eaf3a22b9cb485c1abd797b67b5f1ec9c5ece7f276c6a187900a5dedb118a5a81f6330b92b3faa40a582249d72e23b0587369d074979fed7db79f0bce358c82ae2e2ddd51731d7fbc24dfab9b4ade39bf774f9cc310480f660b7183b128620b5ee411a589bf8b920f641a4c7e45a9f95e62f25508890546f25c4b108ede97685a995d2b519f3759fe7eb41521f4d5f573b31e0b62fca16663bd99ad1df13e1682905b5754ef9786b8c33d47466667e1f410f7eeef3b93e56cf0844d861374f00e35b768f58fb87cb6de46b1fa6c9a22c9fdfd21849ceffc5ee366c27ac6be8f77480a534743b5f959be7f2fa094da16518f22e88bf18cc8778f99d5e2acca8ba42bdc714c9f2884763d14a272883e2d5aa6cd0c50f9bebd9a9e71eccc7a798cfca87e6862433dbad2aa7e6928ee3dd7d7334778653bc41cc066d2182f966e45bf5ff09685861a508b1cd4a8d78dfb207d28086b3f4e8298508b1e618bc8ff3e98b976d70f7adb5e3612ee1a97d1529aa7410ad10ac535561958e0f7391988d9fad0d037251a087cb4ad9b6e696cd170ca5ac0d93cec8193bb5fb702772897cd55c52cf8e59889bc1164aad7706b5f9654d510c1168ef5e42fca11ee256a6c60e2b93246f14d76dae5a983852c3ba931f1b87f9b527cdf13800c276bd959785250920d52a888a7127a9d5429c659086c713330890f670bcda8f347e1402d338bff8c7d078cbe34affd10bf28ab54e026712ddb526db67fa6d9c66ee5282e12fd74682e31c6a1333f7a635d71a22e616ab812bc4835c1fb58d1096ba24239c045bffe54204dcd4a40fcb6638a1cc910250923505360aeb02d4677edc8e7ee732f3d8a88c0a32fd9313391d421a99b5488f5b744325727e04c5a40d21435412f5d8b9a246223ecac0c01f1031aa21fde08ddc5aae261b893d5f41d0ee0ad6a76f2c72fd22933daa204a81ba0652cb50d7f8e4b0e09b5d95e547c2f92f45c346c01b55a4c290346afd082de795ff2189c48bbd9de15f8fbd36df973a6e8b6ee6f071424925220e44fedcd0ce564ca22c47dd19daa9fe2fa9cc3b213d84f5830d10b24437e47ec665c9e9c480890893574e777f020f0cd8b558fd2653986f95c0c7166f9bda984b57fc6465c8b40d09f3034d1e70c2d692a081c0ac0badd8ad92264cba3b17e4d8242888eeb7085ebbeeb31e6da54f6b13a4d7cd8a0c911cd4212c4f6f3d1eeb53b58422c32559fbe8c0c6bde2ddb6f6889ab3f18e5fd8bad5c5be76a518dda0f761e79d257f3b89b91c488a78a617660d499e2c7dd77b7aa99894bc2b0ac8b8fe549e7692d889991b7facae3a9fccc79ee0dc5f5b8c39ad8e749bef027fd240b961ddda78f5b295d71a136decbd73a84eb450d63ccf8efaa6e12a3928c7752ed11e3d474c868505b70cb78c27457782a4f52b6ae12cd593fbb08389a5d2cabcd977292ed746eb9b371d5cb30c9c53423a48ab4734b98c827f28ff02fe2f2ce57f8ea891d926979a7ae63f25099565cc90670952d73bea133a3a51030e820c338fc58a131b5da9d372561ddf6b17a9d8b7ad39c7aec9d803df5646bcde962e3429cbddda53cb2c119bb4f484f734e1db63d44166167c46a96bf39847774a03d84daa18ea21d0f39c5e6650c6d378224f333a18789cf3c0b4e1bdaba4f37243499adebd66903e0c49a84cb2b17f75ebb43307b6a3c6b7f52b01ee7da762344ec99ebf16c687bc3b4d738a565016068109a38ede9d1cd9f7e86ac35bb61afecf3938765376f40ca9ac98dcd515646615062bd3ba98ac598a6cb2ee738616f943f2976bda1ae0e1e574276489b3c632a13f9b2b5467cba027c126c87e21b5942d2f333067e47a49ffa910e758792c03c8914ab16255eb88bc52473f879728a4e789cf16912cb0bdb7364211a16b012a47e5d9d036a9d938d1fd3591eaaac350a34318ec62053c07d39b469b8b9b72bf9586ffffe2adae3887fde58647beffc28191bc24985e5b79798f5e89a7fcbe42aaf01b61371a5dc9df0a351368488bedea3574d3b0876361d7b7235718767f2995e927917fd5ad5f7a13afa05262dacb89f43329bafec62a55ce177f295ef69f7c36582ffef34d554ff66587eb78d716fc44ddbb3d65e751775d77704bbc49c1d7af6594c9fca33cfc4cba4cf5912d3b94cb4ae7bed48a57a39e095a2875fe5e620f9fd0af2a3eb398169755627364184519f743e49fefcc72517c78735c37829349938b65ad02e8ed4f7dfdf545b6bebbb42e963b8675fde3975563892ed0b535d4747df41a33f0a2e6c9b40498f828c4928d405a370312bc6d2d61ff74c4328a9aab13574e08848b9debbeea13bcb02576e6b1d8c3787c249ca047a6cd7e17b2b3644cdbc33c5c7dea5f873fec25817da7cfb8563fd0eec0bf57fedf568e428a8526c0e5061a3c2601a1f8f6e08ad4f4943da5559b361224f7c784b2c7114b6fb44031e0c4b65ef7cb57cfdd0363fe3a07f2bb51351057adfe5ca911e276f946f4ce0a4ab9d95b02ca0902a14aa6414d6ff9b6332fb9c7d70afe964c049f438672d7fdff0e5715f228ba7a9bd9e1879457ce76901b785ed35a2e63a86870bdce82b35b462d7d05d9ef75afdc3ed7e789fd33f1eb6fc74544167c2beee8d823522f77b2266f1397cb7b0e65f2cace6aa7ac5d69a1a9b2ef1148bdae75342b4adf97b6e2cc77d32dcb61fb8fe25fb2d8103ca6fc5e830b1f0b0d9b9af416b4bc7ec246d02ef84928a7e1cec0db2541e35fa6a24bb9259c7334fd03e5201a00d779bb43eb31707a8a5c2bddd8249329533b7d7082b5309b5ee32693ca76dcfd3bf89c8c8eb67aa9b9c90e8240aedbe6547799bf4d76b8bcc5a3a55315a26a2517faf2ff5c5f386e9f637060eecabfb8cd8d35ca729a1e68cf279b4747932f21910e78d2d9d9eba7b3596d93c7d01d55032d6ca99485f960b18d6550f1937083e8f861313f1c5a4c163e04908fee2a476998817c87501636df8f91ae7fb044e89871a4117ca77d8102d5c56c6ddeff70fecddb2ba63a4043a37bf19de64daf11e84f8521f6327c1da20bd43d0f10ed9f4fd3a69bcfe7ebcc5f7f2de9b748917abca84f98651b0a95b42908257a23347d621d635937cded3b7fdbc3c70260cbf1b529cccebaceef44d8104c8d3c45c72f2baaf6115e176483be75b963459328125af5d205aba282ba090c7d9abdefd4c76cdfce620dfc5a0c55a7e0a567cbd60d1c219279db592e7b0638ddbe2d8b730682d5f04ac25a6232a6d0e2f9d34d65bb07cfdbcffe1b3e6b7913efbdbe2016ae609a049bf0f3fe426daa3a9ebc0d0a9bfb502d5098261a3ef4ef7169ff264a77050e0230f256390fa0ef1b44a364f0c596037917c8f871f967b6b9fd47dd7ef3981f9cfabb8d11246ab556d8c33b9c26ecc75b64ba4d94148d63f896f860a17c7d2c88e1e5bab0f961d5a17dca1d51cc6967cf35969ab60a2148eb403bf9b89eb2aca697d0e94807daf807d362e15a8961ff6f71e1ef60af7340e26552c7826f8e786758b9982244f4fbca70de4c3a05ae27af2696b2fd543877d09d15211db5c5915a9cbd22255de941245cbe84d92e89f1301d7ba948d6840a5a9c1414a669c3cc7618ac27224bcfe3c71ffd21595b9df49f647e2a0cae0a9778f2b5e14ba1f15763c79bf581a0991d51ca48e7bdab019874df33fecb9042c989d36cde32e0af8d33f31606edb1a0c88ad67c317bc3ef68073f5e4a35ccf98e0076649857b8d60278c53db290d068be377f30c12e5002b8710478f6ed52e19511e747ac73bb01dc3688d3a37bbc0582d28ca1c0719f7ce8e9c8d2fec9c55010c7cf1cd8df8998fa921c7b4746f7a6c31e112495991c0e476d7c3ccbdd16b5f2fef4bd7161771d2997bc4cdd5906c04baacbf839faf9b029111d7dfc05b2939b56ffe8e52616db7f24ea4ec4cc0b3a9a788a542f69228d4a4eaa5b86f905444c6b35bcf3b7aee536d1571168ea8442503e421437d267c1d43ac9fe83491149ee6b1ca270b13255a44875144df50b223d9c868403ea7daec72cf16491b316d04756bbc8878cf17599767152e5670a1467b60f15c9749a0133806b3d0fb9f25a110ea3f2a8629fe207c1b87aea21787d98377868bbef4f661b197d2ee15eedcde9eebb025dfd5e6de8fb900ac6beda279a64a69a915846d004c5c0b451c52bd0922cd0f0672ed54492eb81be2b16428292b53d94a8ae3dbda7e2d5247a493fbcdf1edbd688027e897fa5833f21ea4420c714b5accff9a3fa40c11d179ca98e0e3d21b35eddb02c1082d0cfaa7d7902d716bb14b74235a375450e1f69c7b61ac7bfd4024e0972cc865a14e229d3172b89b9f68bb77e6dabc3a126ba8a53f5b02131d2203f7cbdc8448f049d3f40b7c6ac286d84f76c3960f997793456a73590b06e3b608ea4de3a9e0bc85f2af9fe1dea794add673e98c2759af7f9c4a631a5405d7a3970086527d17e44541b65255f7e6c663cbc966466dbd1634cdd57f1c65d5e464e7717b755ba645ec1569ba7365ec56a2b2142d8383648782fe606de66305f37927d749795b474da5a617a2efbc4906459d6a6e775d6b09bfe09b7b31598ab4e26792ac6276a29def1648f01c5b3abc175f9c58064c340e908db2f1989cb774a1a8d1b0f132c77371d19bb3f9ce9dda82e4cb3be3e62276e84476b54f16d899a996d2d2eb0d06b234bde7e32662e52d8dbbaf56adcfce7403bef835c9b97d5f987595ed074a94b3ddb7db159702dd819c496c6937ecc0f26d5d74433459a23cb89f024a2af2f8caf23344fcd6a5fbc11b25f6ca17d66fe19e1d488f440653fb81d23ef369913174d11e37a547b49ffb1736d84faa3d90e587eb285e2d8b7c97eddfc54c65198d5b666061f3889f6f6fdba2372339213b76e7a68445c17a41874f279c9e3c678a4caef1136a6a22bd6b4cade8d9ab42ac12251cf74274bd7ef2feee8dbfbce7b7f5019d3f6b32871b9119e7ef0197ba9fce8aab34ce65136f197c67e24ffc9444f03826dc8f67655508ea352a77597cb0a5f487e7301a87c45d3ce0b1ef85bb4d5ef379a7abbb4a48d7c91b20894aefc79ad1249a8a236489f7e8c69e7db71f4e8a9dbcafab7f79ebf760d8faf0d6230b992866d7c64efdae796b0a346c8b8ae53166916eef1df043d0dfa093c49fe8a1edfec8e28b4a8bbf2bea07f580f7ebbf1783f8c577df6a68f57fdebfff8ddf4faaef134dab92e4a24eb101b16bbaa455e69eef3330a64814350ba3dad3bdf99538bcfce915a77056764dc9eb5593bcbe32611f7a018fceb22292ce57cb622a094bb22a83274834e6fc183b68b8c58449d9386d508fedb173c499cfe2ea426977c58ef83bb382eab3b61935954ceb08036aaf32f99be58eee32a6c6abd4e56770117fb38520ee0d36c0bed3b5d2d92b2a85aa0e7ee12f542674751a610751acab9db48958faf38988ad30408b70df79be9329f233d5b6a4cc5be326c226dafa2630c269dfbc9fe90b88883c26cc852f756e7b930e6e5227fe8a10d9a730291575be17b1fa56958af659e27770b2977946158e9f7d3dbf2803606aa6950d5d3fac0b68187981991ff986330e436eaa484ced2d4b1e02422baf79aefbb72ebe8614fce198949e98db3b25fcaf9f4aaec6670a4e7f0d0eb95eaccd401ec95d52c4879874050c4504c4c182edfec0abc53dcbbcfdd14c5e8b73612fcc51d8422cd1988113ca83b5ec22d76d0d495cec218bcaf756b2b5015be08f0bd9702eb2f4ddad68c53574237359e274e1c8a0413a0ed7ce9a1a4930f762b2b1f74293bc65148cd746219d9e0f20245fc7f9fce9c7219290ce1e8ffca1e2a8e8f78e378ca8b519da302669f8e7a75c94d99e35d724213722e1ec843c2fcb6ae8e92705416e4a2554317fc12ea293f84745901f8ec01f32229f89ee7c15101187c90b9a1b43e44a7d39fbd96834d4715c28207a19f6d99a434c6a0ade2fc1b7e67668c8ea913cf27c6773cafb66e672eddcf2da507de5787d433fff80194bf91db7653e27cf9c7fff670fc04fdab7663dcb6d0419dab2ce67388d9b7bb661b9632727f18e79afd3c557b2659a8d0bf5e13c3316d03cd45c3d940ccb0e960b6a51d6db8a1169dad897e758ca30a23d400df46b717a59427d8b8abadb4604134b1826b0a4fb2b8c360d9d2cef2ca000b6efd01d3236734ca3ab8204d6731d576fb04f540f761b2163bf9049982205dd3c4267102d53083b1f8b058a348d8175072834ad97a48bc2990d39c89e17079a74c16c0e457ebff28ac733b80037d9fac98fdf66f2fc20c3994a5b79f4470c6984ac7d8182324939691ef6b419d196129f59c09dabef5dc1354bd49a65ec14dad17bcb0c8b4e893501c0cd666bf7112c995732ef86005f1ad5914f9ac19efe076dbcb48a4f9e6669ab6c92fb5ea19e44de88ff68f61c0bce36c55abbf5161abf103f3073a7a38a40340369dea60dbbfe410aad4b0df156936e68886ca79006cb9bb112543dd4b9b929a7dac9151ab5776e35b07ac8c1117403d18e2d7dccbc2e4c6e3a3e55ebd0adbacfb459cb64134dde877ceebc0713a5e889782bdf70b98416ce769ec1dbf4a577bd2f91ae9e2cb6acb1dac13cf4b31bf11cd52fe29b15f3f41c366bea47f5c8e3fd297c2a4a02f15d7c3d162f827768266e4ab1aa4cacca5292473fbad4201ee400208bb6cc2ff3d5ecf0867fb9f8f72e80f36bfe58d3e3efc07376443bc914e70765df2976c23508c3c3c78793b02bc298a0f383a3dd322e3d490ff3a257c91d4e9323c6f101a4b8e4e19ff61018f13d10ad17dfe3c63d7e139278461b784a84f83dd728465dcdf7767b7367389ed5f5913f7a9ea3a883b3eeb8d30a6253fde333d905b5e45a7c664c1cc80c15961c0c3d9268a94c9612f860144f8bee2f82defabe6fdc840143a4bef3379fc633844be4fdb3cebd4f1d6f8cf8e89bd9beb7bec3a605e74a570d996429871bcb5143eea1df0a72a2cc0cd5d879a9da3fd604257fd38af6b03776c68c82808948448cd7e4a939a6cfa9ea9b61f05ad4df5403f9a23ee5987587906646a1b452455f29fd6047fca4668ca457f5bb4cdc643574dbb69ff1debdfd43269842ec6ea7b813b3b00999910b232b7ac15c0975e202dde95e5fe6ab08c6135882c589570087aa7ca3c37c7183c1631aebc35ca4f276ac39cd731dce71873998d0e78e47eebe142ea42233ee98904a81ffb1fe5632d81eb48108921c4ef671c4fd2e2d893ed0c3f21e5f8840894d8c4ca7c46bf7dc7bd3c02fd5946ba54d879f72b937a711c29113f6a3dfb9491e1b18a34fbfddf9f2cad2df72855a4eb81af9e3842435e8d0e27c185b58af47fc4c2685d1c65bb648f65636e0acca621f2af86ede91a5ffdec0d2a44a78c5ec1079a6fe309d641c7f742d12722f0e6ba8ac1c2ad722b521f6a2d67a55166c9921efd92d51437bbb9086c3ce28ac2fec1f39e1f253adb75ddbad12b29446d9f4640d4da0f5e0b7ec9225bef06d5b9d7ddfb6e560845ed5edfa7b4e1af22d638345cb9c973828af6e14b6fe2e75e279d9caeb82ab6388540eb8777d1d2ed7ffcb22aa8a9a5cc4394fcf98b8ef0bbdfdea3403a70973a20b72f017073495a7a2742b4417646dea773bacdf1e3625af8db72a8ed9c3cb9ec0e230d8d10a7e39ec0206ba6ace9ec462def2ae68532302dddeb11d3cc4f6a768b768e7d0be82a9a1ec4605d1c07459df3b4b7307af00743409d893042edf04398177da496acf0d828b7f7ecdb11dbbc5348dbeaf4eb95e862df2f458d8743c52997dbdb39ae4f9d0539e8e4009a6139343d84f947f76c3851d4ced860dbb9e5fecee5d5fca071c7e115b5caa12a772ba77b4bb2c1531e03ceef50aaed07f29676c91e80cca7af0a29b374e2415426306fde6b77f132722d41cfe5a6a2b58912ecb92c1a7383e6071ec4ce79d048b23e50ad38f587070ee07da11860f8e8f7c8295f58bc40adde29c719e713f6964c78d3433d1a49a44a539b7886c92ab65cd7e28de2d62c240fab5c06a102c0baf4adb40ee8e67f288b285f4889240aefac90e54ea2bf14d8733e9b481e49a06bab9a5a40c11baec930dd66ef83559c53e4b9fc49accc21c4afc61eb750f055b7c0343d010056546acc29bf5c01a82d8f9d6e7d3ca65e3d90a8ccaeef74c95ab3e9b86191fb4585ae716723b0b13f2597463abc83c0467fc24b9ab40fe4c3eb5539362deb3e48c95cf294c8c8108f42bb16621817e588ceb0c55dca7ec7b3a6ff343ecf36db6d1a357219243618c6d6d2faee27382ebe2fc59ea362b06066410921392fd996821f9d4b6f7e3c7bf4063086a8d658c63ab35bc509c1962f7e353d78cb005fcb4fdc593e9454b11d4981aebed8cb59a81539c9b465e46278725739765770ff037b93617cca8cd8f418bdc7ec81f3b46a60ec7094d5d63ef5fc3fde8a78a052765e55cdf51d0823f09caf43b4fb48abe96337409c33d7f7bfc433de5e3c589c876c65bd9ab52f25f35609f395cff9a5947be33c16b9f99b1269e9557de4166102664b6fd3c99fe316d80b97ba00edc2e81d95c0c5f9f598944e48071e41d03c023e3900d1a7843987228e89bb01c59d022b559e7889d39ff68f81ddaf1705a6583d331e6fb838e154852cb6c918a5fdfb231e0ab6f30a6ed40699c6efb32828089139bee5de1147477a781f2d169279d8901f5908329806c54df02d4d072c7a870666197cccee9fc13825e7fc1e9ae438ae857f3f6e7fc02b2f01e95e99c4f452001dffd48fea94f83b6986741ed890c4e57083927d48063df74c5793b0096a4f618ba9f1b3e625045ac8c8ae8401d5039894587e3611ea21432cfa6b1c97e27230378576bed2e33ed3ada0d679a4d7c68b7ddbddcabfcad88c3426f69579d96707deb5776b948325050b3c24a2f0cef857fa510022eb1d1ca0ebc11cb68d484c21b5c8f376286891d62e3eb0746d13efc64434d2ba7c7951dea7df70cf4d369275c65c3571e439c4222907d91783f166b163ee12dcfe2f6b4c7b847f89262ed9c32effdf652ccaa2f72ede7ebb17d9299cbbb981a94566c97e56f226b4433134d176efd0ee6a435460a22662e6d23e1a983155cd7a89dd45475276f6019dd63e2353b4be56d5520d29d23ee5bc3b5f2b4cc9bd5d757c04f2fe2126775bd4967fee3838d1226241c2df26b525864da140f8c9fa22a63398bb671cbd473257996ca14ce7e0d7cd1806887f665b353ef3ad9702329419c3bec444d0278cf8f434ac5eb42abd112d7f89be72d4b7367fa67203708287291288a4dc87088b28b3a496b8e5d7cef4c82e6d58cc014a8a60d3e77aea0a782576ccea78e0e663f628363b74187615db3b0977259e8a8a5f6abe38add0286301cec7096d8e208cc15772951ef97516ce849849a453e719ea771889169d8ef1ccce625f94e9965933ad99b35af75ea50a4aa396f9c5ac15d6b098c44545ef19ae19e9122e9d7d24c1e3ea29f6e680d1e8df32b11b6b5e995571aaecb3e799ee266cd309ef49cb664e2f1e4ed1e691a9c2aff3f000b40f4bf5edfafce1698ed7d3cd1bceafd7dc883671d145e6141e74a82fc034728f6798248227c010fc90109f2038e154b8e0a352974d3d8fef9eba84b86afbe8099e55ccba95054b22ccec1bc4219fc872099377534f19f2767ab8bead29c7d66c916da7df2d8ab3e6ae74b9a0c5ef53ea6aede347afa85abafb9c82cce0315c5480f5fc943de62e603b9cc364dda976fb4aa8c74a49a7f3ceaf1a25bbef2a29bcc43f224629c37afb92565d49baaa027dc5433dc5bfebc62160c7ad1ed47af7ac5f40b8c7ad693b7de7cd74e4fbae57ad013e35137d6b39e7c57de0cf2f5d836792b5157ba1b299a371d4764e74d8ffd62bb738567cda3aec1a961d58c2b008bb667f7d33d6892b3b55d0224c21c1a7ccde88b2140e3561e83a131066df50e8e17d357b28a7bbe372b460c2826500e6bd45ef5f3f6aa7b6d1716f4f939fafb60d8b15ce9ce3eacd08a51595384a96d9f15a9ca337529809999be96932d16a5afdc1e8f674e49920dd0a6ee53f7f09364447a2bf679d52d33f4f05605cfc2196ffacb61be5b7d82a1c0aafb63c8219cdea24549189be0df32ebb84e147c1461fa38d9736e79a1fd6a35e9b617f261e2936c7bb91cb8e19eaee80085be9f0356e325d010a59690a6a58ff254e57035052a5fcb9dd0e5e1e574f4d6f97cf471b66d7d58fed55c97cd35b91242fcb65895ee2e46d341aaa5bffd72dd2da3c5eeb75a082fc4584f833ac1f3fbbdeacd58b7ba6fb0f7ae1992971903d2bae9e72cf4e08c903ee5a6618ef087117f309a7f9dca3f027af7a96ffa99c684fefb3b875d3078d4c1edd485b5d811df52c08094e2edf473c7298a9c2bb945905a63553acd39c5ea48662fd5fbae7a6b76687c524fdfa1170f85571606168b1738cfeb829187e49218eeb28fe9ce39e9625c812f9bc0566a84d1a36c63fb0f985e2cf630bd98bd35d273e161a76e3fb89ef3f56ade0d5c95d11f06ddbc8e7917f7f2b4dd659bcd2efbe073246fd981634abbee9813e7b01dfd7b74752d5de356f9f9a56380f22199e5190f72e9bbdda3bb05b0270fec0a68f5ee1ae07d9c03f52795b32ca25a6717126b4c71c478e67afb0bf9b919be53b9fc509c00903bfe99c604340aa3c80b30438c0853681bd5925f1ae8a6881ba9bb8ba2780434d6491f1aba6fabe77e84a719790db65ebc6fb58355fdfd0cfa683ce4f42c1bc3cb829589a87915036aea3566f53862aaa02c3b03fe9be05c17142bef3f064d70d2ed8259f768ace0e49a5183cdf4a50e196b39ba5b5c7b93790446aa62a89459a0a65f57de3539133341980e7dc6e37ce905daacdfd791e0c3c70798d4c6bd6d2fe11be00b7d5a7c6487d760c5073ef0be6f820eb777cc09d1e4ca508848ca10e4dbf0af6ab7eab2ef987a68fbd1e5eef2dfd86c3e1f7f101e7d54827a696d71a1c247eae9b77820fc1fb96cde53ca914f47a65f5f2cfd72fb33f4b7c0b0408537f61eacf7b7c034636cf0aa17deae94b3bb9dd46bf2840a2adbaa485750d759580c66bddaa1f8cedcc1b0db7dae31358347fd20828e2a2914534b62857fe40fdcd70ff24906643075b401fd25f39de5b5d6efca3aa2fc846438f1f79dd7861ab2572206e25147ff33c92bc3f2516672f8c63cffb1cf9c0364a9b09575fc311b7ca2f6a5d78d913eda48a7098635db5be6abbd901a417d8398ac2c78a60ceeceb0d6985e19d96574e71acb9aa14ddcefe7e4690dec32b2e96195c6964c3ab1a72ed0342f1bb6fc5e6c0ddf61ec058df37650d5ec9b957d098c07b6bddc74d34d11d2b6b5acc5bd6e0f4d1eae610f6d7a74a14695062047d3030bf71ccfe78bf7c21e8f86159cdc55347613a3c18406afe794264f93289d62a6d4bc9c19ffc554c62814d5f41323b8ef55b7b72eaceea9836965f8996f8d8883308c8bdf03a0d527063554f3b851f6059834ca0cd5193d5c98092c35d6da6eb2351f4d5e4979076379d5db56f52bc9ab3e1d8f5fc51ee35b045b41cc5d81fefa59dd4cbd41914f345a081582170577f461ca1fe6c8bdb63687c518823276989b17c4cfcb9b7e587fd77259bd403ae5e5dda29a2f7f86449d812ecfea1df7938a9ad4fe8782266de8e748c7a37bc5e18c4d7cd0e66956671f6c54a65982571be251f55afaef8688fc5a9e052a9d21165bd4e7b168f3aaf78c75767e6dfbb9befc4aa27d36af7a33d09bc16edd102e3f06f29a4e1ec7008bb48783f27946b259c8c908fcc748840fc257c565011e857fc824de6d7de96eafe9c42e7b201a78a25012a79538cedc23febb72e913dc13f9351e8a95c922c36aea93462b5aac12b4ed1e6c5467c79c4feae93ba68ba1ea4ac1c0787493b383d3800b715f058cec5290d7029d1e9c3564ad4eff3bd1cb7b347d0883f39c73bdb7aa91deac54638d69a2337ac5cb309faf0cdd18b8d8043af03897c14ba3bcf96cdc12a40d662f2cf43a4a27d593e367e400b0677017407bf96688afc7dcd7b6c6793bcc770751ee7d65a4f7bde9ebfbd3d3f804a73dc93e41e3e39a6004f73debb16fb9b9273006de2f83a84b8a1132c1a780e2628628a14c0b35441806d6325e2edc5af2aa36b68ba2965e8f4fe0555f19edddb58113afe00f33d4235935b805264610e36762829965dadc843079a8186bdb489232fc78f94d7c2ff4f7312dad98faa9ceabde8db52aeff2c17d5b3cb478fd9e2e669e314e03e777fbb4c2976595dca41da4a38f4857fd2bc099001460c437f49b8c1e9f8d963f687326827df7335dd4e33084b33f2fd814c3f064def434e6049c4dde5bd1351f6f6d6dbe09de7bb1749046b223448436b5975856cdb252995170a49a8549719a8f3e049fffaef9056fdd1843c37fc616988f495cfb4b6f26c8aba45d4453d84e5d92b12d32d03f6c723bfd7cad598df2c30c8166ac37afba6d0dcd60afdc70bedc1890fe158abfa4b3bf406cf0ac1e481bf90fda873f64482d7c036e1026e503f29fba136e7d19784274a273d5cfa036d206215c3065f1ac6c46e6a1057b33430325d2ff2295727d080d4ef8a6578875f25f4cec9a0fd1958c81c5f87159c34d8e31be6114653b0e1d130663e4daed31e123d94ffe1b8f7f3bf59cb06c577cc57cbf7ffe9f4f31d2cb0faa81ee41fbdd66bd3643179b8a5dc2e3ed18ddcd702edbb9acf3ce70c617fbf4a6c7661dcb2e6ef9bd7897361cbbc69b4fef76690ce894995fa529337d310ed0d16eadc50d4d2e50f619e9ada279d35bfea4fbd36d0f392e4ec0b83ab0575daffa6c76e687d12cfc6c6c31470c0019a61b59af80af6dbaf7aeec0a19255c43298c22532d267732d9ff21b2610e9f64a08ccee9551726ef8e78b588686596131cc2e3298e771500a5ed34d2f3ab5a1d233b45a9398531af996ad0e79e549c598f6ecde7cb6f71afba2a5118db5e75bd485782579df7065ecb0b60370b6f2c33689009806c70cf49218d11986f27971158c63b88582cff1e6e718dbd7d424a9e17426aff19029827ade0efbcbde97d3ad9e7100a03f502e9f8b592ad168cc5739eb3c145d9813cf952911a1722d2346dcba09f3c3299cd7ec4de5098ebb4bb2fc40f03940c6a1ca4bff45f06423a4cbe46f5972a6dc3bfca3d2f96daff36c63a7aaa3436af92e855b7bc85be575defe9609835cc5c017108aa0e80c6a4a5b47c54a10cf36c8d91bf60acb42906a8264c597eb2ed9984c1a3cedcd1bed8a47ca99033d788d5cc884aa7c21e39276123ce1cd9ce7810fbb5f5a59e6cb447de075bed2f193ceafbf17155e766a36c71f4b78423d0ca6fec2f85972369936872d829263cc5b50894e7f61be9c94ff6bce3ec35e7654dcdf4ed78d135d03d6ca781de0c736cb6a7b1e39822ce7d78edf14193ad057ef99d5f7fb7e434a7c4a45b9edaa45beb4b113be9b563b3fbca8b9e74c7726fba467af3a6b7ed2ee723638d44cf18f63523cb7e43e1279e183d355e2cdee5a33c99a0be82186e28658312ca271b4b074509d57ce1ea4fbe9299a85c2e5f05a133e9bb5c3b0c601606d3be57bddf46f8ccafc714054efafe71c67a48c326fb0003a2c64807ab7000982ad1af6c444f0a1a870b0bbf013fc5ab3e5d9657bdf92f4e5f217bd5b9efe7e23d7d43ddaef7c2dd0a7f3c66a6bd116f5121e63ea560f09fc89b7c9409fcc728b5ed3ee30b72ac56dad677a104958756ada01fcbc42ee4280bfe0fa14bbff9359d860cbc40dafd026930211ffa07f93abac983ca4a716bd5c9af21e37279375e84d003affa3edab170fb189f5afc888697fd85fe2bc2cbeb94710a7fa4730a94098eb4179ef188174b17df6a52eff9f6f6a42f9fe10dcb51dad46d4f9ab16efbc158170b574690c6e121492c71a170de659137d40b94a8f78a296c0377ad8c48db13a2652b59e7d28427351087851b7845234a8726d3f173861e229f2071f25f79ab4a2495f6e3429b958e6b5375e7e3ed3b776f438bcb89017eedfdff9361fcc13a394ba768e9effa41ca3e6cce03b693e74ad4c75fd6af5f4f6b9cb632f793fbd2a66bf5be81de0cf3d6eea898dfe0d939f931eddab6f8ecced4eb3b83b91fb77433a42f45ac71beb9f9d4ee531c67b16db46f34d02f86912ebece25d947e1f4d0728d2d8e7d7bd56d786667e7c7618d8732589884c239da88ff4c0d70844aa9d8a8c5b4a6becd0ef0cfcbf8f1963f4c490c8dc3bcead4f54386139e56d8d22d6ee5c48ee414f37753063c82a9575d0568c8040510c9c6e3e1c425c405b53e7552fcf2d1f25b306803a3db74fc5c63fbd2c1c676bd127d39be0003dc6f00fa1bdde268f039463c67a655fcde1818abea3c21902814da0e811ee7e543e7db7d861e791f89ad84d57ce74d37397273992b6ad3ed4b2f6da5691941dc271c42975675cdc6fe02299c76bb0828f52959a08bbc28931ac8376eacaf429823500d091fe455f57f69d6bde9c6eb0176fe7efa7e3c4c903ec9c9129eeca9febb3ecdd07f06a07fc53406dbe8d593de8ba53dafba4f93f68dabb12e0ccd281bbca8eb14b93cf9b0954c030f1479bb893cf4af722bac8d516fa3f668f41c201fb625eab04b065ee814a2ee4ce7c7a0dd27d01dfe455ae49d7ce741efa49db62b557a186a29dbffbec0118d527c3edebee3fa19ea2e3f0610d3cfb8a886633834c44928c71cf5ccbc38c8e2660434fd4ec12d293ef6b4e99760f27516cd2876566212f0dd97ce73debf741363da43831c1df274ff60eb0ac6ee957a60883fd5c1669c7419e71ae8fc74083f508f81cee1fdb62fbcf43de90d0f17225b99305b071723c6d88ab1def5d534c7365b2cde582fbcc91c65fcf9f83c9a88c864d91d4c032b03710fa218258bc97742d8ce08bef85ef527ae7bea2760503c8c1a2a1c28cbf034f09897d3656cb7ebe4cb17c5d951a83e27d2b0a7626faf7ae7c0d0abde8c75ee689fd1423e3468466a8bf7eefc99a7eebdf7de1701db6bc56f8c3cbaac37c015dddc960549088cb7b5ac471fa4fd5b3c0479ee4fdb756f7ab737ddba5ee81e7b1dbe37dd768337bd87ad2392be400acdd89a4203f82f913cc6ff4403a958922c09ca6c118f78c81a7e3ccb656f69d373db9bee70f0c3ea29c86232f96f7ca22b9f295591cbc7f665375d57c99b098bc78b5af59675db987d97e48df6ddbcea3d63fd9c2f9636a3ddeb87709930d0e9005c6e456fc0d05ecc341eb683e72888cd2823a8c96d706c68dc776ca355e5c007858ac81f382e296377310a584a380d3907852ed2a426cde8072a8e0927f7a6dbc9e0513f06955759155fe7fa8c8e39f9275b9095698ec8312565c2a77a7567e5a449cd1fc1e65ecd15ab0624ca9b8e0f6e9b2d2f18ea6e51ef1be9adede626c6f9e6660cf3b318e21e1ae4ad1ea37d7ee51fd7cdcf5ed7c1699c346518e6cd406f1ef4e645c721b9dbdfeed264aac5edfe4f12c7483c49c393b6d1d0ec8cf5f61034ea63b2b1f1f3f4f1a00ac8ff7a646aa202933ceac72d24dda3198b61260de4b432cd8b6b1801b75253c6fa217bd5abb7d539f7d7377e5bbac5ab96f0d8e9a74fcff1f4bf23d608a3c56061fc252e4755e66a0fb00ad44303de768c925ff214c6f1786b399fe355df9c6d3147e9f9d7ab7e3d1bd59fe4afef55bfeea9a7dc22b08f06e2ae07d34549b2ebe1cd40b6a54515b83b84720be294800023b9a6938f4cd7fd05f9cbf187a852b0d61719abbde9d425d07f19e959531ffcd24b6fcb8bed5753cb21f4a80eafe1b32f90f2a4292f9096dc483f10024fc63ed2b04a5078a49755d091043989f9a3e41f768a3eca9bde7540bfd38fb0907ec80172b55d73d8a74c519c4e9c2155f77af96ce0d0e11a9293dbd98af3b5f6a9b16ebc1e2c6f5b60fa758327b58f8dcb930e1d2570680c4db10ecdc252c55780a5fec8a373d3f25f31c41ec0abe797ada833104fb8f765e08336212085ca3238ce3b515855d241c33ce4e1e4aca4dca7609d0e0dfdabd805d3bab1ce43b423c28578fd8ee86a28becc18e049f62f673e081cce21f08cdf62ed78a9f47db5909d74e695594f33be18834bf48f1e7e0b1ae978d3fdba8b46fa62cf93deda6f9eddc47b8e717ef66cec8275837c6b6b6bfb39cf79cefcf21e4f30fec14383fb396e5fe9ea56f96e4b4beaba7433cef5a0f70d74f1a0616edc97a95666f949c23e23f124179cab8d8666cfe8b27f699ecf358e1793b7e889b6247b1ea378300160185777fc47f10428da94ee61558786d21baf424279fd75f2311859d16107ef7d05a6fb5ca3deda0bdeab7eeacc999f6798071d2c8f85842bca52182c9193356a032d39e0f79e2ceb181f41c0abbef87de8c59561bbe4d74a7b9f265b957bcb84dc72c35b8babea999ddffef6b77f05607d87c8d44b2e1a2b90272d8e3db214214132826a99f7c3fd7e0cc1fd675ed3f05ed71f79ae95f2b91d69f2c5887b0d13b7f8c85eafb18abc403a1abd567a386143adf058b19d0bab8e3e21187492aed04b62ca4c947c16bafd8068d348ef7bd32d5b0fc8d1d3d3f1e41de942aad3c0cf30168354945e2db7776482774f4aba33b66d8569fc56f83f3b1ddcab4e68a6dbb12f960acfe04d150b972f48fba27ce9013d1092b6f155de334a5eee0847c21eb488245b4178866e8681070a8dc851091dc21c6af094a3e48f9cc2ae4565209da79f122ab3484a0f3969a4af2be51b0f69574517e2f53bb2b3a1e2b262e0252f79c917608f07c3529db0d636a94e86810e2e8aacdbc69027b62983a996e3bbd1e3b3a6c8ad27cf76574a78899474cdf956f4825ef4b39bfb0df47583dce618ef8bcb7bdcc4f8070f8deeb31c377575abbc655d9d46793bf49e370fbaf7a50cf5e5a89fb6fe7cc2b1a27d3e1df5db6a74711cd8ab3ed99cfc6bb4ce47b4329ada51bfa874d4f596a9f81bd3a884aa86b8bb069bf836dafc299aae8c5d0cc83e0fadca6963c83d0a4f65631c26dd2febea164f9fc6abbe18df5bde0a4b191bc0f41ec57009c096a7903a62608bdeb48e04576cb169eb5528dbc0d2bceaa358ea7b5e75c6bae4bf56ba9c2fdf11e103b6657e810cec9216d7390b6ff02cec55674d5b4851c7b7bb13d6f1da15ef45e0a2bce959580fdef43dcc9c7f0a09a95f2095a72416465376bb48bcfc5b61e8780fb991176d4aca0756efe5c5d1f2631ef1290e68ebe3c81cf6b4bb58fc3b7afd88fd7bb8380e5fcb40962983c4e117caac0b685e9c6b38f10480dcb75aa4f79cfde87dcffabe2d3087bd58ea7543b85c18283ae39208392576b4599800983afe92e64ef4a1bdfc789259e4849bd4078fbaeb1e858c19318e2165aac999baba7843baf89fafa52197566447da11ac73f62c46fad9358ffad10ef57ddebf23ba1c8aaf220cc0213f258fb4bdea7294bc147b2b4c455e9dbe32c44af72be334bb059dffbfba065f1951ddbdbb37bda1a1ed4b6ff916eb4537ad916eac51fef9cf7f7eb71dcdc0bd1a63efa71f9a71deca9a17ddd832eb5bddf9c42751b1e7d3dfaa6d33d65705e8946e5ff23d5ae54e06ceffa5906005c25e366a08c6b1bc18285e84d668b4b7579dd5dc45f7aa3f79e6c95f9a2ee60ff0f26a4d4261de3d2356e62e8345804c179c05abe9e45fe677d5c9ac0c5cbdea23adf572adafcaed85b06f4171c822a25a9dc7f99e7bee79292f21dd197840ee24ca5f2c1784998893e504c826f3d2525a50341eb30a1fff72daafef4d9f2e0edbb75642cb7cc075c731a4181dc923fdd8f4104623bce92f87c76e0f4dba05aa12c1674ac52b74f22f388669349e35a858d242435310ee4134629e82b0cd447ffa3aafa1a497fb5e006e7887e7df4b9a293c3da56f0df3d86a19473e894f3d2d7c8f543eca4132a2b05cbc7a32d9fbc5d2de5ef536cc2a5e7fb174f0a8ae50f3ac27423bd8277a192aabc57c5bb8b6fe49f732221bb9e18aa2771fd2f5993c756b0662bffd5a7aa07f21a4640c9ceb49f7492848570e95cbfcd2b0d451ec2cb11ca2c50a3ae291c6d61634702f663f1ced501ff6a8f7f1f44590664be22f6862445fcb35f093721e875ccbc35091fd62332a696f53ae43cddf85e3e56b8f42054ea1797f2d1e6f7ab7ddc56bfa467adfeb7c547f97abbc19da171af70df2be917ea1067ac3c38109bc553cd35843b36788a9be35cef89cf3ec377777767e944f1cdea5a28faa0933a88848a44c868149ac26e829b0caf669335ede0ae37c1dd51fa53ac621ca6ab3671c7a5fbba347f95154beaf49d09c58acc3247cfd321b12167c95c54f99dcc3f8b717888c947f17177415b83c09af51bd496d89703a1816ef161ec95701151eede5eef46cf6aab3479dd74a9fbc7eb471fdc6067bc776bb2fc00066e08c01ebe5c266fc4c02f0bfa704125c06680d3d709b7be9701d252fc4b6d0e8ab54b7ede1bd3539ec2d361a3cbcd3bdcfd003f77bdef431def4ae61ff4b2fdd42cd9afe7daef8a4f57d2dc7798174347e4fb88b85a2dfadd6385718a244a56348c7096245a8ac48d63a0cace5f25ea764cda3f5cf31d2d706f53b2d6eb84609e72795c99fc110f8205cfc775c00f84ba78ee30bd4d30c52c4cb2755692c9c09b451a12b1e08c116fb6adf48eaaf95571d4fcce6ce26ef24c9d37ad5c96ffa09af115b6ee67eaef1faebaf5f68a4e9516d71fa1d4ecf1a061a7fa9128ad4f29c0bc0d20f5d213cd77851de3c09785ba333674ea6ce068fbaf8ccb2bc700b2d1608959f610d59443809e79942bdf25ef4cabc73143954061aeb48f12a3cce7afcf06f320c1ef51592be381237ff813ff01f1f79e4ff7d101eb94d769167e208859114efccfb3215a1590bda0a368ca71dee8107dfb7184f1f9c4e463a737e2b8d7b271fdfaef3a0def4a38cf4676abcf686be28c96664af1bd8179a6ff7d7e26702e43a5e9f495ffbae6d862671df108b571d03f6ddccfc679dda651ab8a11ceba583c857a2bdac595c5446640c4d2f192f5f8dc19185c6115ef5995b4b7a2f6ceebbd71e7ceb5edd18914f9c3dfbab80c14200d8e457326df120e30646c1949165701b05ac6685baef7ef1b2c562cdab9eefaaafbe0113f86fca953905467166aec57bd5274fe14dff1abe1570871a5d18819e8b85b12657bea9dea1d572c1274e12732b4d971fe1ba8f53356b784ec3434e4ce61ae931d4dd0749be3f2b77e5ab0b83df75fcaf6aaff1c4a9ad8dbba103db47445347932850d427d9c80cf10223deeff9b7091c2e2ccc4d961f865e9f704a3eec0552eab2c06ab1179591ee234ccd7bde399eccfe01433c249fc75b0f2831d81c57835c3838c24044703a90c26d9d62b71cc86e1f4fc77f81ea6c8139ecc7cb35e231d6f36d75dbb5d08cf5961fe2670f03d114d296a04ed028f7c162884eb9fad7451a29b933ba856815d8b27a78e8bbdb0e6f91d2c1a32e1a40b8ffd9972ea62d02dfca178fcdf204571ae40969278f5e705c5873a6a7e9e0513f0e635f8475e3b774ac1579568275acf8a38786d8567a45652d992e47994deaff18ece3d16d08fd4f307f7c372dfe0a3afc7fc419f4a55e7f35868b6d9c8b838b6198afe3f29279d4d707eaf25a1f13bc699fc5abf6214c8fd785453408321b74e686c66f144fc72de47df3bd38484565727c0b2cf49748fe13723106619ebe579de27d418e3b3faf3ab6f2683cbf4386d60e17268d94e260d249cac0d4516fbedef921259078d54773bcea93f13bb647cbdd2d7d966c18e1a5573f0133da20b5b3b9b3dbed55bfc85ef5e5bb0a22cec227d41d9c497b4f0aa9608a6f41cefd782fe3b364bedfecf984fa7cead8efa6e7b2ce9b3eea7de945227a24b81069067b2bbb96635f208514af8632fc69fc4a29e946aa18904554954ab7f288d842999080e34731de79a7603ef2edd1fe0ba4f4718c27bdf619faf8d2de8c51c06f4471df1f5ee918a746162e53f1ab76697946fec1b00b1f550b1edbbf11981e04f6c791f729728f577dae29d7bceaab45dcba577df0ac4a89673f64e10773153f317ef8cc453d0b31c181cf64350a30d8552066e4920a12b7e7b36dc523edf4937cf865a0bb2803c7ca1338aec532988fdc2b5cce41c8972827aba6c815440b3c2cd3ec22aeb254b4533cea2dd3622d757fbff160685ec4833543c9d5868146cb17bff8c5bf8857fd01b8e7f692da62226daf2c0ae5b7126e669fe2ac9c65b69a5f3a511f6fc19b77168fca86f4b6583c3ec1e712dcd07cbeb33b9a4dc63bd353d78d7677aa984f008f6ebcfe06c660ee6085f0e9873f9d45827a25b39de3bb108da790a11ca0989c8872ea7cba14f7416e4001b04d81a7eeca2f270b6f57681ff487b5b57c92d47fe5c5ebc7d15a8f32e6efa1e33e8633e997c1cfc3cdb86e717570f2f3855e77d211345e2f69d0105b33c8e255c710705b058605340e5e6509fd729cc17f33f62444950326150530930698e6780d13ff75de40f3aad367bcd1dd17603aaffa756deed877bfcd485c8fe94ede19b197ea3700ed7e00905dc20dc24222b098d26409a3567514a883784fe1a1e9325ef5add1d6ec6cf779ace553075f7ebd985ef5bbefbefbeb81eab6025a3854ec0024a422bc53fa8d9f15d4dc558379b4fc491a3fcc715edef439bf6e04dd22956dcb0b7df443f07a08befb6daedd348b2348c323094904df2796c3a45da7cc88553e1ae9f29d6fe79792e39ae5e2fdc803cfb3099182a472e29a233ce9ab5f2c9d6ba07b203bdbc8d3bf420a3e12be5079265106445e240c1b79eac6e8e08dfc0ab13cc78ba5ec557fa52df49ef3fb174d0e2d3af6c5521b0cde55b1f0ec85ec8396ab7817a2f19ca34b4b39b0682b5d4953b07a82d203911d4d8784b36c7d39a4f890a281e620a593ad18e4c89eb84f40eef39499d86532cb278a39a25549262ea3a82ed83b6ba79fc7d719873dea7ba8bbea531a91cd730cbfbcc5793f369772ed5c02df84cbc277a5d2c373cab92cc682dc6b8a116dd4a58d1bef4dc637527f9307bc99838637a32d6ea6fdcd0c7333fa83f4f266babb9927fa376393dc8c52a17c94360c7033f304e55c471ddddf4c7fb4351edfcc42347d20053733eecdb6b77fdb61a4d387e92ee63af419d7d276b9b8857efcacf7edac155ec66ddfc51dbd0e387e8cf9ee135b9b9b9f79e8377ff3e738defa894f7c229fa26c8677c3ddb962fabea4619fe17a29465a33d21d22a4e591f7133c427d7f8da9d2818caeb4e0035745f24519952428f7a2aca8a8b336ab4089321afd558e733c1960af7a85dcaf30993d2ca6ac693af7aacfa78bd13bf82ce35986cd0415eb49f8c80ba7cab23ecf0898f2b065024fb97fb03a0f1df1aa8fb7673e7dcc8262c34f4ab2c8bbde2fc06ce7a5ccb56d3a7690d083a7159d338669df19c1caf8405119c0201dd8844e60e90a2910eeaec4f8717ffcc692f3097ad30f6bbfe64d3facc9500606becf174847bc400a5134849ca49b81547c4423e905b1ac9796924d6349d940963e365f8e5d601df0a65bb61e50883cdfd9f3a437035d23bdb59dcc166f669cc7b1b2aba8f37408889ef378619a114f8b786305523e032e2005c0c95dfb5f2c5d7daeb10d93b8bf05a6196b8377751f8a2e79266e10f929e4968e6524fa785cc521bf415a92b489cec8091aef81c6eb076ba1f65c9c3ae1d69781e6a24f335cf433ef817733e23b7a203104810e2e96a2bb259505e70a458abd5637d69a7eaf602fd5bcb07b2543ea6ac5409f965ff8c217fe2dd6f70fd4a2bb9b4b9850f2ee9d3c84f5aaa42be7b057f1977aa073e6316fc486c82232e5cc434dd773bd4f80741c6576923fb9cea7c376dd9d9cabd2c2b2cc7154bb60087fd7a8e92763ace69ccc715dff74ac4e4a206d3f8cab8ce457d42da22c5bf41cd78cff8e91b9a9601646ab697e13c57700c3db80e1d77ff3b77ef3e77eeb3fffe76ffcd4a73e75aae1ee5c71c172e9ceabdbbd5443344353a3b8a5192b5ef5edc5ee0f83a647d4454162b029241d020bf3214a882856a9537981f38e21c6df3e9ecfb3dbee80577ddb3d26a3fe5e753b58dd7383673db61121d30f5f6e7908c3f5c78ba0c22513337818ab1a6a94b4c735299159ab4931f502afba5f80d9624111d7069767affaf534f7a885c66a39d1c128ceac6cb1e9738577dc7df7cb01f256070684f068b029b306777a60ade9eafda28028f11ed264f1f760d8cfd3e0527bd30ff5bd31ee35177c8114c397177fb975f84ade8ef10d652c32277f65b20e915085e4c377a957f18cdf91a4ad7d4bf39850463a9e743ed7b6586c745ef4eef9e4beeb36ffeb683a7e5fb10567e00a3cb04b94ad6ca3224d395c655a3e2ad1a1583ee331e9727a0fb239f3bbea13b6c0101aed575ef536ac5b604c3763bdc5ad7e882f3506643e75819496c73afa76139cc491ae4ec60629dc0f073deaa5f0ce0c1ef53e9a8e4f972216fb99ac8c0bdbed32e883c2701e4c538aad8fe01cf12956af3cf075c6a3edf4c1a32ec2be4842f3067b3b381ff9edc98dd7335f3c804877c1f90359869922cf1172788e8cf962c7b21a323f359d6f0327a51577ee5da0dec0e6e77a3bae673fdd7c5063789975fe93765b8c996a1f6018aaaef5ba387eec4f676eb6bf741a08580a02e065cc804e4f7e1929e3d945a7bbd2378339bf76379c31235f69e3bc9adeee184d273fbd3bdffdf8d6d6ec5b34d2699817ac8d0d0da72db6acb5337db183f7753942f3c13cc5bb0b778796853d6001b384f603475969d1daea22342d505212d712087813fb2dbe990cc6c9d978acf9817a2be3b1aec4be73eeb919bf87c59435bf50e0643bc1bbf1389e75cc300d30d6f87241411c238a6164762be531891edf08bf564ad5abd89513036a9abdea1bb50d40af3a3f572a847e01a6f7f2ab45093d785ad1a1b1f702c0770b5198531c014b5839b093052e610dd31a87e9d350267f14f87fcccec1fbb1c65e0740f61863179ec89bee7d08a3d79aeefab8e6235f2085462f761fb094907ee1358945da29d87ca3a38670e4228dad1fdd0f7ffe071387bd406af96161b1d18c74bde8f9ea506be636991c2ceb7e78b99cb358adaac8a052016c91494ff2bb1c6539b2296ff9573a4fc5ca0b48a3c5edf6b0cd1618e3f5c0e2e1c8174bd7db0ef94b830127416757a7c0682e58abf15c4684d43edb94ede26173725b03e5a047dd06a8bfc1a3be86a9e3b2253fca5574b5c24f50e639777323e9fa4f5d32a4f6dbe9c7a9d8a746f16e7557af47cd83b85e3ee4af6e0cf869c4dffddddfe5f742775e8117fd21394daeaa37505880fb910974b8bf54ae8e57ed6bcba8ec2d53c5af428c1c7504c1b98a2386aed776bac1de4ba3d41c96c1681aab04e6ce30f6904bd253b7e2d4861232fab45ffbc3a99834e5fe65e0c80450da942048a9b3bdb60df531f2bbe65de75c2a04825c155ea76ecb38e993ba05df8f5f4c7e786b73763fe3de940138291b7da3bc19eb2d6eed2e66dc4dbf17b3cbc3fb6a465acf400bcda7a736ff31a8fa9857e551ba780b61a483a8943944ba494f0409e54a6a55b67c254aecf9991070e0ecee6e4cc96fe633882c1fb9225e7598b41909abfb6ef0acc71947da79f16cf608bfb1f821c71336e95864960948a78286d653170385ac6d9cf4fc277327c65479d5bb479081912ae12494617cd38a1f02237035c3b679216d7b68984ec7af629c5b1cb016bde0497c063e6125a3e667f52a5cf9046377270248cb0f727ffabece69a4d36ef522200b99037bd3ddf2d2037225dfe2b9dd53affe9a4dbefffdef7f2937ff6a9959e521af14bbc83d513912b00abb9ad6027d4ad552bfd8fb44207cbaef0552cbd6c39e377dbd26f918e7c8c96e8ecdc44ff38adadfee208167604b01145659a632260a5a983f9087f194d1ba2164849753d9c748d0285ff7aa5bde023f8411fe6bdef416b7fa21be4418086db3b08ace90ff145c274ccdf3d5c409cd654929ad6af10b44e70a27b4d3574f52ced5df17777dc996aa5aad50e256468a96932215471648a8a53d9482102b25bb42cefe29637de7cbaad921894b69741c32dc50748931b04ecf9dcdcddf9bcce7df088b7db68ca1e2336dadd856998f94fd0e30627f2c29a133647bdcd9f1a17c1b8b872a74837d741d34133f0b7c1998504f85d532068de4b2519267a86c5d29eeaf361a355c6bbf81c4e16cac32b2bddd92d446cb659693b66d460c4cce4ef54731edbb8b72617aed1a7b919d1bc62f63befad5531b1b5ff7bce73d3df3a98487354719edadcef862840eb28bd1d5f17d689c69a4f55a45aff86d6dbe13f20e11ea3c916d2d20adf184e56239f33e17677200b9cdbb601d57de88117c17d5339d375a011927f6efea5388bda18bcecd603c2ceec11a38f955a51f90a4060d5ed38226a15d454e68255d174950963ae12dd87341bcea7aaaf1fceff25d758e18e9c0d9bceae0e382bceac07b9ac1be3b77061ec55330234cc21000a800aef09feecf8654ea11af8728b99fa4b83da7a14eb363bde9f6d37d37dde147e293a32d3afa7c60f5351b305a3f0045f202a94a72f55517f9877c22ce790f824cfbb1131560eca3c9f883d0cbad4ab8d3ce8d567f1ecced57fbbde9b93a2f9b6e62a49b7baec7172a3e75eaccbf00b08fc845f24c789a737d6bdf22e0f44080eb4b20b400568a2a98182f78b174f44a8d8531db5f8e7bb1d48bda1618d3c3be65b1f0ec04fd5fd239b493a61916ed108556b9d225d0d12cfc3071c5788e70d2ad2f03ad45694958e969a94140b66ab9a46123be8b360bac79dbc79369bb033aa074c27e23dd2fbd9cf657cd8e0c8347fd48d45c95158d9e1aecde80bf0eeab6ded96cf7abe1a007578bc288325376d4406735c080c583e43bdba738542e4c4d57dfe5d29e8e68ab1da701a0b91c3f00b1ecadeef0a045374f38a005ce818c933e2ca29e0bb22d47a5d4c986738e1daa97d2d6f6ca8199c8461713f9fe631ad93fff1986a2d8980e4b1b61c97c1aa072111504ca1d0b27e8cdd8a63ff3e4935ff28d5b5b8f4e3c9ef7bce7c568af867b467b5b14b5b8d53f9358309f95a0a1e6401a6b2d4d3646dcc6a98d9f27cd67dc62300631d69005bf4534f19be66217635835458d584c0df9974328bfe71943582378f9d4531886fcb850bdb029529b11ec35ab7b6ff0acc7e9b883713e9e7d93cc26c10b166a4d40dd3c3186c0f28e9daa50657cebf731ced24f1a2dbf0ccf7f1614b4182d4fb72fc0f4bceabc3cdd85c028ceccb7b855f6e3d964f25a46f38de9c013064e830e5a1910c81a33fb2326de49c323e8e6b37e31becf69a4d3f7ca483fca9b7ec48f1bad8cf53eecd76ada5f208508b71747c92cd0274cc319fe696912e824eae5f5ae3c0a703c7a683e5ffe03f1e7969793ec4db7ed9e91be7f5fba5e746cf3fc8adc829f7b6ec717289c2c77df0c047eb6b383c387a500439093962cfce4ff00482361dffbb398caf1f8aef96cfc476ad77216d4fb5d7e5e4ee0c1c0eadbeacd9bdee26a319c2f0506f4c94a394348295da5a47b4c3bde73326b2f0d978e4345454b15440709aa8978f6a43b5f068f7a87fb927da8a0212e354a2124efdc18cb420943af67715fc2372afc67cd5d04e94a94b926776bdbdcba76fba38b6964ecef79c85d0e0cf4e9d9bcc1c231999cfe1cfbc3ff1744f83e794c5b265b8b89a3c3b52760bf88b8aa21d6adfa419eac2369eae454a729b5484d0618d2f0af86b74fe3d21f675b2610d51cd1b2b691c519d304ddd84f3984b89af1ca21948a120987e2cf39c8e0565e9381c902eaeb59535752cd52119162a09a5bd57c55a99de76d06de5c4f5a2450ce357f1f63fd4f3cf6d80dd3c71e7b6cda0cf6e73dbd67b4b74591c3f7f16efe4243f07fa1175f84eb543979b11403d2ef36f3725b4d0aa6f3d7114214baaaaa1fea912c1806c12b64e01ac216f1ab4da891e255bf2e5f6ea4204670aa1ee5dcdf07de8cdfc362cab2b8c0d0ff12bea7fe0659d09f71778272e84c5402514bc5e22b59042ae7298d6d3824b117c8d700cd5ef56d5e2adddaf3aa5b4f60974ef70598f3f3aa03e773e7cbe51b1c437e0aaf0aa0b32a79056502b0b2e224cf878a690b9ec0e5d6a37f2e0cd0e19c863acd56db5ebce69810c288c71e7e83d363aeb926aa7c81145c7f8f34a9007da41f9ab2790bf8c12c6ac33494495b9516ad259c845e8edfda90d5ff667a2bebc7eb5b5ef8827f9f0e3b1ae9fe829c87bf16da0efbb08ceadf61e077b9d0a32f4a55c7ca23c9ceb3a1011725edb9bb97f064574ad3ade57cf426647336f6c5d243b6c058e69886e655d748d7d33a18eb85974b756e3fb063ffd239ba24fc5713a779e95be599c668b81f9a3e53554d9987eea73b49183cea8552c5ca4db2995a14fe3cb5287324de732d2a69a4cc591f422c47857fa7118cf578d72d59a78a1e758e6c44237948e81b1b87540f45573106d0f50b8f760b78d777cfeeecdc0323bd82f9e5219577d95aca7bcd4be1afe87ad8527ec3b6305be2cfb91836fca8416d1f6997b9aa46b26dd943f0b199cc619d1e219bf984b2ea8ab36d8c88b30d46832bfd718d82d1c196f16858efdb3182d7380125589e1ebb6cd76f0779400d50ce65b44d2b61ef86b2b3dc8bdddad7720b83f31ff3a3247f706b6b0b63bd0c762fd3686f8b20e5c7c3f28b61ac3feb867a33da8cbd094218869bfe55e8f09110022688b112a28964b156933f980a32f3e8dfed1be211048b706ee6658b9d9d3fc405f9ba4a79d5f15863a7f3832bd9abae575d63bd0bb97f8dc806cf7a6cbb9dd9e69b314f6e0ca08ca9a1e27819db510979112b09992410032757096f47f578bb97cb97610b7f194df7bcea4b3dffabb06e28af605cb5584b4cc7e33780831bdb27944a0a84a11e0d290a5921f61951f80db98fc5dfad8c4f238e0f8c3378d38f47d1896ae1f7bbe123bef5da29263544d25d1e9e917e9cac489f5124325ed87ef15126e97f63857bd3d3e05c276ca6e64dafa6a78d62a4b74b35d05bdab819ecf919e8c9e487f13e3c94f18503e0cad31020c915ec9eeb4952816fd3dc81623c19df86417f875bd4b6c7fbbeabbe1a5663bd79d59bb16ee560acaf50746912f29cb36248cb49dd2569d5c7cce0a1a1ba8c943a2ed4550747319a3ff0f9fe2a3c8ff3b018eb64455a609c977c45d2a850c248775b8da4445752b481287b1309d3dd2ab34a142562a79f1e1df375c68b625c9c07d987a6cf02069ac168ec706e7fe1f74ee637dc7043e9fce9f4ffdadeddfd337ca5ebdb30b23f16bd5d275aab0f22f9c87e3ccb6aff40edbc648d864ae633948645e1cfc4991154236500a7860a8d7d6ca932aa4d532f8f53c57f1d7642816d6aae4c019dd7e8199a26b6b1ff76251092a6807631a0d465d52ce3d85eb885bad4572ecee5ad1d03561fb4a964d786797b3e9d7e189b72a6b11e83fd86c762b46f3dea9698dac7cec5ab2fc134dc5b762121f77021175ee8357da3b8d747bceae4df0c3d50fc524a42e8c126164b589a4e1a965a2e1df4ee88e4a0530f038b1aace857b1b431bdf705183f599e923a69acb3a2d47377e0fe85cf562dc650b9857de77785811a05432f889c5858602e89c975923e80264d926ccac8d7e5a97fd572b33cd7ec1966d770f70518daec79d5d9a673dd41181b5c344db8f7de7b5fc418af6d8fa61d433c35787ddad060104aebeb54ed60ee07784924061f93f43e830fcf673cfc1968efb4f2a6c3e86db1b557bb970a418077f0a6efe124a90ff202299cfb3ae920454215393174b3acd2e12bd92515548678f9c501b6a02cefb559fd02e9621fdd2cef070c5fbf33b4ab710c3fe4078deaf1b7337685e64937c70f882efa87651aec18cf4f0004bf366a494ee133f94da60b78ddd9fafa23a5ec76f797b6e3c5df46a66fdc62e1a0514e6896443ed768ef2da00c23a3cd801b3cae0d33173f96a2996e65b564a428dcc95344f54b689761e1d934a8f6f5ddf582e7708550fab85a1c7f1ee85b621e877964a6e42a8bf4cc78e03eb4e1e4bce3012d4aaef0a84792f24e7887680bd6a8e2fa9c63d8a37e3c2f7eb1d5ae3f25e97bd5bd573debe8fd9d8df9f6cfeccc77ff6758eb5bd1001fc5407ebcc41d865437f0e7bb8451cc30a3bc1afd10c3975af9937631bc9d0562a1536679bae05a62e7071d9cd125d669ebd99ffc4cff692fb39bb43e652488695185f1ac53473ed7e43221b44dd9640128d7a459fa53606a81a0e3d5ebd323c63e1d54814519b72d1e84bdc1813cdeca9b90dfa8b1eed57b06bb1ef61bb225a6ef5db7cd3331d603a29d3c5b41c3cdb134385b9a6c59d95b4f7c1a1afd680812e2833c91e3f3fe68221513ad49f323444537110e6e435c63bcea3b8b9dff9e56e555dfd8983e352e8f75f3aa53d70fc181f058d8606af1c66cf62e86cccb7ea11c70650121d384b8c266b2f2216e6a0a2681cb9fcd84bd1806af7ab7575deb8940be192b66d70daf158c0d2e1b1916bb3b2c6e781c531cd422fa8349e3fd87ef18388f4701b1569a020228443bfc9a653207c71cb1ff7ca70f14ccd979d3dd99bef70347ed5748d77edc2834b56f61e6e877d50d796d46f3c9e8875c64aac0e229903138e4204fa54048874c2835955198a79b9427a30f2d1693dfb5f9b9f6a5db2646ba6df966ba797e72d713c7e9d59697e649d740bffee187ebb89ef8faeb63b47b996136dbfa7978e9a3a655b4f2b7fb7154dbfa268a0deb5e0232ed5c77abcc3b4720ad26372fe69357cafaf562e9de76179aafbeadae57dd71fa5e75f3cd68373d848b8781f2584113481a3ad2b5dbe67c9aa90e4b5afea4dcfc8a3fcf09426d7f3967331a0cb4edd0aac6cfdc81f0283ffc296f25434e42d120a1957a3eb450e0563679e763c9f697fdaab72dcf8fd9f9f28c8c8a93d07968f3ec63a06f283623f280571db0dae7379693c9bfd898cfbf73b958fc71a6aabf02b3f11edbe441a6a347e33885dda2ff5116d11dcef82a07985156d426f3fd16ad8eb4d3fa8081ddb6223fcbc1a9cb15a4e9c7120de362683b293ef757d8a65db1734d99e1344346b4693296577b4915d7b518ddb5558cac9d776ddcf79e61eccb722e4a3579d715c9ab04237014586688c7c99863317e3d66d006efe35dd737d86d56c6fad6c4bdebe69fe936987583d03e9f95d08cb71633285ef5d3be5dfb6ebce82f87585b4568100846dbc490c77e1806eeb92ec483b1205def3a448570b3e5f83b979bdbafddde5eee6eed8ea6dce45c973a9f2ccfc7a1bdc1ceab6e6c566e5a058d4ae17af39bdffc352c085e26bdb2ea93822154fc4e16724d91d75488475626cad9b4e059c708a56a8dad76affaf2cd1abff9ae3adbbebb2deaf1aa63fcee820fbdea7c3de5208c76f1f6b7bffd2be8e73bda18b2afdbf5331e6985277bcc48c7d2b7cc0b69e1adcc47e38fe0d1fc8425ebde74cb0c4def6f0127e9ce9beeba60d1aa6cb6f2b29b69a1d1b68bfbed5b936b2eee5e20bd4d9e920e2b6f02bca4c281ad13c743413e3f22642194538d21130fc1333f2643e505d2be3bf3106cea4db7edde0f1bf1cdf40d3d6e65a4f72f89918e71be57f695496aac9b4019e5454ffa7c3df0dc0ef0c8289ce57b252ea6516a42aabcaadf34dd013941fe4b9d097974b2bc6b3e9ffdec74b4fb89d1e80c72bfb989d23bc023dd1698913068c4e9711dbcae85d38b7d569dc509018dda64a5e72c7c1a3a8643c3898db0d13d9e566f16ac43a5230243ddef339e60a3fa405b68e0c4064e75ec352fa1badc32f5053fa5ae26c08601ef94fb576199dd2ecb1146fa0e32bedf3eefda10658d5e1ef5a3c80674585d0000400049444154b6ee7dddbb78485dcd1868c6fa4b5ef29233d258af3ade60b572825e75f2f5010e143e9f71c4c7bee3ef5f7c1cfefb386f16fdc862ae2371399a8f17cfd9584ebe747739fa12f2cf8f3f09dd9e97cda3433a635cfd101e955f5106ce2628188df6cc4d9929b4de2a088c87b68bfced580667c014998973281da5b6d414f310456ea7c924e33932327901b07d1992f322467931f2f2129c4d37466e9429fe0a42623ab2af2c26849bf133ae71da6a639256412ec7b7809d6f004d3fb385c17eb6fbe292f833f0b2e9e839bfff39f3e78d9e37e393c3bb4da61a0d8cd3f00427efe8b28566c87500844e9b8f6f7e167c7c40b3a5292717658d481238930917c580175f1caebca2d8301c687efb72777c2bf882a1b646bb7ad59f1a476d81545d0deb0b94e00178d2a6c1359d4ebe0fa328440c2c92542a0620e030e96821a0775165b25618877c98c12a0aed495013c6a3ceab0e2c455719a1af5a8f85317d2ce7efb0c3c6cb2250782da83f923295fc9d721a98091ccbb3187a7ee9c5b03e56957667816a1c85e1a4337db58fb9f3a68f066ffa3e941d9af10552bc92df23fe8b8f48c13b4d39c82ddabbc523f21af493a852b3666d0bdec3f695983c277a811457b932b00acd9dd615f4b7bce849fff55fef2a62a3b74c8bab0e1ef8348f2c3f505e0cca0454ee4ec4099855c24d2ebda1181754a5ac9af08ba5f337b12767361e5f775e2f960ac5e079150b1737847ef22009d3a5f31c03ce9429395a597854bd578aee1840ca9b7eea0446ba9d0c745592300690a9d818e0c4b44e98982fcc542ee26d55c60a34219bef2950a49ed646d7562f9ddd9f522827e8f6c1404bfaa85333268eaa1fcaaf7e0c1ce755f7eef4ac632f6db35772bec99649cb98fa51d9d338e6268bc96314fe163ae0dfc1a3bfc07cf00b89496377fc02ba2279cb7ac72f8da7e35f825f1fc068e6183d00f776b169cbeb307dfc31d9ab9fd896fca495ed92df7d00a7d2038ced0f167dcf7867fe5dd3c9ec9b9197af9e8e27df89d83c940917b9c914c660242b30f196b5a68ed341619bb2aad48da6548accc5df19372af51aebe067b57dd3ed307e1966f4193e55cda71cedf8423deb97db508fe75ac3d89b202c9e3efdf47cb631fb5e8ce147a3908241500a5e9a61a0a72e1349618ccb347668b0a0595c414c3493c95f5f6e8f414e7db31c6ff554c673af7a67ac37affaa17bd521c037d1dbad2ac17ae3d8613ae599d59c20f9a7e6643cfe02aa70061e63094c075c17a2bbc8b091931b1d13e9550f01dda88b4db57270acf6aa9757fd008ce0ec8f31e09d0c90d0438580e2d174e52768c0486c5922cba5fa64f4e3fc4c112c2428fbf7a65bd60f3bb5e525c2a9914e9fa157dbf2d26f4b1a2a8c466db1d3d2c6d77a4070ef8622378b077939067a265ef9a394015c113469d842299ae75f1a9158f03ec1f85fda406f7a1a9ee0e4be740f786a7b74da09fa706fba5d7da5063aa73bef7c1b9e6be55223fd2bf1683f1cbab62d32e3f1f47bb985875ca816c434530e0013d92323b7c973dd3de46acb6c1d35e7d382db50a47728a3b0d48af769b00ad41ff962e9aad190b82818283d615745c7a2a18bc78e09d11d1d59d129d15fe1e3a2e9f1209c39e12f1e0d1e75f0e8fc82a8b42d8b7ace35da23515618cc77e992c14ebf53c5179d3c8f3691de6dcdf5686b2faaf01496c5e998ebade460dcbc7f076b8692ab1d036d11f612bceade4bdbabce7c9e174bf5aae349df69eff5697996b1beb932d6b9eccc745e06fb85e003736776ce8347aed3de319f9a9fea29cc615d4b1f8c6db7d736f58c692cbc7c25ef9788be1187d377e1757f24f7100558b399f3b18652e530da95350eed2cf55d2d9691d1c9e8d69d8d8d172d4ed5f32b8d76c6d86facf3a2e9e8339fd1de7462dc67ac9b3f4928384ed2f212b5c1a06bc6baaadce97c8677760ab368c4be4f84a89ac46179736a338748cb74cfe411a3276d507018c3eda6f886e7b7ed6c2cfe152fadf926dc5c46a4cf6d19504624f21718e720500674ec9813946d51f6ef697b4bf5250406731ee48992f2f10dc03997494be3004c648b32a085094f7ff65bc900f6c7e1b681c9e81b30463e49c12e04f685bd038630db5fe66c7fd90723bafc9f3281dee13747038b5d76838b973db52d40d6a541b51df96b96933fcffd7d9e9a19f1b1461f867904ba79d3d70df5756fba467ac61c4e2b0cf80ba4d08bcf60421d49123ec1c84d46f995621dadbc0a7ac5b3d9b5a716d7e4f81ba0d5c3b5e565762e9afd617e72f94bd85342677379ddb1a0cb147b7fc1a26baaacacd3e9d0cdc40e04f7f03dd1f4655784d8d5ff1370bece5cd6c7c0aa74b62d2fb923d93d32421fdc6bc9738703ef78bcfcec7877e3ebc79bdb8ff340290b0ac4a0c1e593d75d17196d81c08f4c64c1a841d7b6c238fe109e39067ef2a7ee8744e80eac4275884495aefec8964fe612645542ea2be103c61f60d2fd0127288b4887df2abd8beed88a678e7ce8eac374e8b98bde9b7bb0c773a0a9c8eac2bffe37fffa579093db9c1cd4032a8c7ac94d3a28479413e7211bd7ac3e55371e3dc8ef2a7c3b1ff9850e1b78d677ea1d239cea182dd1e1d0663edbde7edaa158b2ef90df557f3ff1c413f39b6eba299fed6b8fe40763bd23c81761d468db3cbc1a913892263a34bd5df8c1af99c02ad9811007a7ca78bab3331d9d3aa53ca71dfc332333e22b28c7ce47e787c2a6febdca27422c372932c5970e1cdbd42aa4bccbedaf5935393681cebb0ee3ec47506ab72a58881681383a2f1375000640f9bfe62db777c640540649a903ab0dfbf897e38fce7667bc7e76663bae307a388b1e74feb237ecd9f97390b1b31728631711b98273fea1795e89cf708873bf006347ff3beb99573373dce247f39d312aee261290a69b41b3c0c7af41987a8dbfb81f88b8f63b37b7c7fff7d9917bd5f156abc13a22cb80221106f50b306dafbae32e287b0d46f72d663271c5022fe5d8b62958578639b0494b4f6a5121309f09af33c442702055c196a605ee6a4febb657dd09ceaf6b9c6bafbaedfe34f778c76a61c0709dbd9ed8fb2f9c301ee214bca58d1c9687a87f8f84463af770bc91deed4db7a92bc5038b8854ec9dc2c766a525c785c8cd5e6f5f442968ff431002f2778b2830a5a752718f912b3b74fc0373143fcb47aa06196a39fa518df494f006e971a8594e97bf8f89fe2718803d2f8e09cd95173be6d060869a054bd2c5a7c54f254f694a9d654a59f899bef43144d6026b67d0a557fbac7bf2e5b632ecb8ce2d30199ab6f61511b04797aed42d46378f3776ff3ac6dcbbc7e3b36c81d994cfd217273d137e85c6bdf1f9f28c2f96b63df383f7b5a1e9e2c421d3a27821bc22af76fa4ade85ba508c1ae948427e95ce7942772c08fee0d1f8245bd4875fa1058fc1316763e52ea20692413b41e4837765b393225ef6ab76ea0aae72eb8b5ef566d8c4ca8959950eda16f5ca1c716e86dc11d543f1558e81e6556f71f3aa7b5b1aeb7eb21163ddaf99681867014ebc8d2e5641b3e5639385e2364eed396f0186b9dc12c3bc84d18e27fbc2d1a3c900e726e2d459e80e9bb26e3e48791bc4aa67606930af3e3d1b2d5fcdacf35328b69b2373ca59048f41a2ec143b672de54e2923b689f3eb62f2a746b3c93fdfe555bdd9ecd4e6e8cc9940036edafc1563fdb11b6e983ef1e816db849f5a9caf7c316d5efea041c711bd02346a1bc353e0e73d62c3823a4492ea8b43e5441c0f9d934526121b8abd422288bd75311d7d1da52bc6c14869e350bc576e863001b3fc2ce8e20d2ac4284ca992f1c803cbc461259c31e5a9366f8e8c5799cd77d58523ed62ea74e55e537d0b27a1ed55379d00dcc7c248fd3d198ccbd343dd32bdca349485aa4caa5a80c02c8c1a57e1bbe5927700963fd60db5c24b973f10c17161b2194f990e5452b0e64d1f494bdbb5f8b06baeb5325f20854e78c88a3714f22286a60ff4a999385c9117c440502db44c045b9f1d8fe73f600a83f59c34e3f1e0dfe4c2adac0b33a67c00638411d29f0394d14cb67d55c6c9bf63ec6a4b2ec36b2408b2b5f4510b0a79b8531ff2966d39a9bcbaab28f37e3bbeb369f8312dad0938f98eee72c48ba5f33fa2e7d580c2eff37fca3c71ebc32f96aeb071f113a19df4eb2628291472a608ba495ffe8a8214427774c98900893bf7042d873dea20e9ff6fef5d636dddcefabeb9d65c977d2e3e39ee71910da98c85a34a28b250fb09a5ea07beb428aa5091e222c187424929043e3888b6b88138c126e2e258ad1d1303b59b2a34c981d218a4525162228470429b881c515068ad9e431bec621f7ce09cb32f6baf35577fbfff18cf3bc79c6baecb3e3e97bdf79a63adf71df7cbfb3ccf18e33f9e77bc63b21d581a673087bc8dc2aaa47019415f0a1fecd20ef8f8d3f7d0ca54c7c92b8d781c8e2bd4429b51eb7791290077519a6ddc834d81e2b1b64f225847f39bb3d5f50bd6dd060348beebb6618dc83317bbb139c477dabb6e9ca01d902e5e605b0c7bd9e36f76f92bccb4e5366f3383ac96c86ab73347754ca23c059931add7b134957d1972b1eb7867e74f98073fec5cbc1b2cd97a9d982fe35ef35208e35f9fafb325d0ce37df616ecfbc95edcbbc7038a8fed5c17ace5b37cd97b361ddb717ba47b05e6f360cdf64926153c4eb1906a8abed2f4572461f0e0ab8f1f2df672ee0e07d88231870a20070c4f6a6213ce082d32f9a66c1346d32c9a0b6987f170064cf77fa5071ee87a583d0b952dc53abde7fad748654fe002759f8d11f51054064166b27b492028f6805a9d626b4148d8b029d0090785bfb02969cd0cc9776b572d3ec3e0493f22f11270073dfd6a6bdea7bed049847fcf2eecf5352037de41738e539b5acdf6a539f74685186e5221cf0fe61d2396f5e65cb4b403a6967ee06235ff8e32b5582f2cad4b86ec233ddf2b302afbbed07a474f61f0f188f48289b5ec8134c89a8441870494165ac8826df906bd6a4ef5f2cdaab6bb6815c08d47945fe6fb39afcfa888172a010c8770614ef0a42ea378cfa9ac6b4d7a87c2a28b6470d3cc17a27908f33b9534603ef495b2d8e7cdbe69ed7c7317bca203d1d27831c7dabb4f2d55789fdaf789b8478efac7c58ca9e3fe7853df7aa53544c1dd7585b5fb6e0ae28f3a5daf2bff14f0985279d79cdf2cd9cac8c611ccef8e2fe8b0c4215b16e0f1f32af476df06fdf924094b6c2c6b6b7f9160bfacb8af43efb5673c98cf4a3c4919cb7640ed06844ddfb326919b34f1d7f19f7d17a5d6446107151ba6ddc834b81e271d93ec945607d1db0df3abd75ab3e34e554df6c5b2c6ab015e6d84b3853eeb20dab706df28847b80ebabde63f385a861f1937f87b5e14eecb3484d10f6e034256c2aa9ed4cf82825d60ad1d2e1a6ce7e9e92fb2adecf90019fa609b29db13655ce4e6b633ba9b5d8fbe87cb3e385b7cd5cef1f19b4d4939e0b7bd39fbb8cf6020de4eccd5aabbc5a8c0ba7946faebdf64ee17a03e6ad46d5368e5beec9d939df73960d540152a91404271c40edee816f26c0d6418272919cc2c65e7f4ab7833f31fe00a5837e1a3ab5a75833818342bc2af82e8df1656003604156a2cd465a43c1709fa2cde106c814eb48684079f1898c8d646228901b0d016596f685b9119dafca4611bcbe93bcd5606ff1946b325680ed6f92b2d27cf87c30545866dd19011b6d06235d2ad6c1ca4fc0c03fbcf266eed76a6b216dfb4e9ec9b5f4b3e79699371e117003d40beec29d135761ceeefff35b4c66f5d32a5cb8d8c41561c0c64972e0dfbc7b9856b2dec74f169827fd9b8ab7c40ca879cef31fbf4060519e01fa39c343b0108715b0013630232d97f5a8bf09a9a60f35458f25b384d8c5bb066ace9aca32f32d227e83229d7fe60f17926737579b5043b4c42588cf0c66177af3e2cdd99407912f49b60bdb4ea05d68daafdea63daadfb9552a08d198e81018116a36cc4f65e634a5b7465b87351b749ad651e8f66bc07b35d7441ac101b9ad345a4fc349e132538f088e2cc7da44b52c21bbf1aa1d5a34c1f09302267db408bbaf2bdb4ad57ceb04df8c051a034e9eb0d3f0fac8fdaf55276ba1de6165b47d8e5e16105d1b0d7e105daeb65dfbb1f64a2fabc6f8109fe8d3a9dbdea136871df7a43c6531015058c6ca8309a7f40badaff68f605e9807592de9e2f16bf97c34332b039773a91396fd917ed6f6240fa249eaca77144a5b1b7f3af5346946801ebbcfd55b36e3e4f8351d9a47bdd8c205df77a7cf945a0f78511dca989e57215a409f87bf9b1977f85018b23760c923adcd1e0846801188e56f805d4061263c668b0338150d062275a75753b68eda2552fb00e0127adfa7c67fe43a8cd0f033fac87f46a341b57f0c820db2152ef76084820795ecc6b1241bdd1d41d1c62ba5cb6b15fc65b38c6f20cc7baec0418b58d7f8141fa5d969f4581eda10cdb9057a0f83ded251a505ad416140ce16abc4844fee9384606fc15c119855a69716f3a16e010f1c3903749466d7a07e9260baf74c843edad99cdfc8014aaf30ba4f01706e7ad50312cfc804ac41914122a1f682805cced0d52fc1f30b6fd02e9de0acf0c1f0d40f61b29f05d32238389726559a9413bd5c567ad01d21610616cb2ad0c2b8fdc8c499ee44bfb09b0b1896a0fe27385f9b69da03c274932d811671d95dceed8c01f09f564b160acaf1bb116a7dfc75817cd84a01c53af10cf0c74f45bc57e3ace6fab89951a5f9a917732cb0ff2e5aedf35849fc8822394fcf55c6227a8780877e4cb2ec4b30aa495c65cf1d097ed1ef5222db4ce625bbf34979a8ee3b8a4bf2c90fa02775dd3a20a9f139a97c6419bbe1277ddeab57cf937d92380d814bf0d7b7828508b32ed02ef235867cecf69303eb1601d99bba576bd4e85d17680d608da05ec7531861f8dc0fd15b9f7d0d6034478998cad7b71c27cc8a5bf85a1c0e96e36e9e2beea75868b3b3bbf6b8f6a4624e80e0e7a5f0647439bbab5832ec2b344a66fee3cc9b34ea7ca387999da3757f6372054ba64b4ea1cd9a856ddf83297f5b795c495e98db20179a366ddf97ff1a8a79deceefe752786cc234e0e6935760633c23361386c31a871c585b6cf3cfe11fe15bc4fff26a206a0f308de765ca3369acc3f0770f88680660aa9092a95e049dddc524738d9eb3764e7f45ff1c6fe3b7d2d62bc991d538399d3229ad2f35a5218df4a4a1b53e6cc8f43a3559fda887f02bd08fefe7c77f65f38655a8bcfbc9b0543b49186f02738b7b416af98856eb6eb74f60c14f9659b779e992a2301437b807aed1b3e2f0fe1c169f2ce34655f90feda4481b93f26186d9d1a495420003fcead5d521a9085399978619d7616a222d7453e20fd3d09c68034c9c539047c82ad5fdf635cc07984ca9bc52835d61fd15cca5f0fb03da650a6746b02d6b5ed43b4af2d1c88b78c96a21ced59b21f2c19bcf53c38496c19c9846ddec86f5fc5e65b0e423b28792b00fd3b48621bb30546777f55bf7fd8b7c0408a0c82a356dd745b6dac54f8528c5200ed554ec82879df79161e120c7f5a7807f1725499bdd81ce61df4c5695aec9687d061ea838ce0bdc36979c993f0453674e08eabf3add130daf4ae52774cf73bbc7b3505deee35df36fd8349814dfc2eb0ee13155877dfba7e01bb27e709d8b5d5b017706f907d7967ec00bf03e0a37507b1b2bfddcb53c7631bdebc938d77c55d7eed57d330cf1c7905d85330eecff87c02263b5c3b512921ed46b863a1d3baf1cee9ce8dccec4f9ac043955a4295c27b73c2a7de0745a638d38cdb5f36d1bfcad1be7c8c1d53bfc66e40dec6bdea7c7dfccfa1c5d34eeafe529eaffe1c9a9a16591734e3965fd16330733c6bc0d5064becc47d07cbac371df2c19a3f057abc7f3cbfe91618a48bd5e03ee4fee1e413940b923001ba29c9a15030a18de93707519d1c47f7377777effe2620f95346b6ecb08862c2d0e4312d01ad68431af8e9659a87ff335a7584228c3e3d39f956de0c7c15552a1f6943067106eb8989b4bd69309b6d9982f95068b1f3a3a994c50a65ae084c0f9fac7e6efa569b3e51e4de1df90552b673444610053b7e0010fc927d0a8e1d5c5b19928b8e0dcebd1e6f48ff7f7e77bef85b26bdca9617f0d37fc6c72d4f59462b1407c5ab21b7bef40783d2519adf74fe19672731cabe9276259765b5b0f2d60223655a17cf603f315fca13c891d8e76e8fdd0ab5961c2b977a08632f58b2e46efe2ea7a7fe62a91f96b62d132826a255f7ec67ce149d8c609d812fa25fe06eab559fc8f38a1cb253dec9476520bc965f5960128c706661e5b706198bc3e93e4e9af73c73f5ed2f5b1e427dfb066c90ecad1761db8f6510126f3fb5dfc5ab87fff49fbe618c2dea314b84d0fce3fd32adfa651abeb1acadfbe1a04081456d2f9f4ab0eeaf6a02ccf391e908d805eb751560d73e8f1a47bb82776fedda2d3b5e6e1c2353170a811577f9b55f8bcb8504f8d0e36c7e57009739307395dd8bb190fe963fb02760deee872d4847a3de26b23fd59e3ba796055f45abceabc4fc6681daa66e26adfaf34dab5e5b5e8ae6956eb4278c3706be51eed2c60ad88736785c22e7ed2ede0f51ee482cd1af83565edd87488e546d952381f1f58bd4a6230c42739acb0cadfa9d3d31805b60d4a9bfcc52f0786fef1bd9ebfd2e19928cdd6e80dbc9a90d9406477f24a3c89b3dbefcba15bfb4f58b7869cfe947a2ede75d704b67681ad8d35296e8c536f1977dbb7040262c9cfc3668d52d81d7458f3347fe654bcaf39b9b4c11266cdbe22db6771e3a5b182c112f27677f8ad7d9bf99641780747b2642a826bd6bd3e34f87352f660a6fde60b1d9c0b7316d4f72fd2c3f2045167e5cfe38e1caeb30278b26682a5f204b165524308d26c056d9827f8bf9e90f9f9cec069b5ef60ba4f0ec9d30fa5b02a85221834713de56a6b5d9116c8bf1b87da597fa6da3ed41506c15deb42d0d322ce52d034d67ca345a396b3e3221f30c622e4c9327495a79a998026d82a527bf6538e869a7525cbd6df4453e2cf5bbe9e1c35290c5a38fce26ad7adac7adb4ea05d6cbaef8ad7d750a345e868119371c3b601606de081ae569fc7d7cc32deb328099ec55305bfe4963faaff4766290be9862853c519154bd28fd593e98b0ef08cea7fee4a9c1b8ec14d46f036e1883277781b62960ebb8161458e77b81c775edfa08d8254c01766d41af807dfdbadf09e8028041ee28caaca1ef89d398aa5a27c4b62f2ea745c74243664f78d300d0a72d30d1aadf581cf83b8367f6aa7ff9f204989673f96348e52fdbe6dc5746902ef02bf047e3c42f1cc07fe759c8f5f16c0b20a406aa8c64992dda8826c8314b2080ee169cf400de6f6f5af5b6a1c3136076771ff15dcef78b7c97a0c31a1b472ccf955398137eb4bddf969f896c67e7479af2ef90743bff17a93f9911365593666a432f9d625bdb8c6b75a436ebf03976ce6ad5798ff09d843fc545bce0a701a2bc7621505a3850e78f32da62c5b26c3bf18b93bf691d98154dbaaba171459414fde6ab20f60bf7a13fafbeb2525e3b8e31bc318b7c1bf35f67f7e1e1fe5f43b9f55689238b235935eb32c936a2c92de31114ffe555fe2264cf30e966f177156d3a59bfcfdcd1ca4b78cba49256ded41ba8c3c865cff14754221fad012d9eac96e47f9a9cf26c5fb2f6c0166f9a3c60e4965ca4415603fa33b008c44de36b02c34d9e2cb4c1c4d3c22581c49bc28b0f4bf38ba516df3f2cedc8222b1712107fee87a516b035af8002f284cbc558635463af2c71cc0977642091cab08b3267b0cb2791ab9ffcb2d5a843ebaecdc9fc22b571c81afb86fd35c40f8f640cc1320286f06b2b67cc26906ea2cd1af567a7fc5b8dfa448a6be728b03eda1261d4aeeb17ac8f80dd6d31e3d69811bc17803f0fc4af83fa7bf7dfb97b70676d717087b069c160bcfe7ee9e6a25db97c1e0d80e7dfa443657eb2abb563bf1de3e87bce5784a9036b7dd370637605561bf14fb4ea6efc63fe3adbe7bebc55daef17f5b90d5d7b25efebee29802ef02b378d88569dd9f9c721cb8bb53525d472ac92a0ce1c50d63f114603ec365f0823600851df449eef40a1ce0930fed0226abbbd93ff84b7f55f01ad4969610d2e584ccf499050cb72fd6f8024b11c1d89e0fda37efc7381e09fa2aa3b612ac545eb6d1eddb4267bc8db61ec841188a9d59ac088967f3d00ea9d04a73c00f35b7617bbdf2ee231b96d69cdf499743a78939acbf22c514894fdeb3484d3019e5eeceefe1ec1c6571bf5b60f8de26ab7fe01e9a435675218c7f9297cc8322b1e953dc65d47b71f90c2f3f7b4a5a234870af247aee0b15bb737310a6ce39732a58469c2c3d3d9fb74b3b0f41bd2159e193e1a78f4ef92f56b3381cb7f8a89bb09074929bbcb464073af47ad5d7401e4896c2356b6ab2d2ac816314b6b69545244b6f2bacf3af833a70fc83dcf393d4704923494a1899732248500bd4a335fd356d86ee8611a2e3312fe5e568acb0f4befb4b3d5d5aaf3ee748526be4bb59ed2c6966dd8d65c9d02f247396deceb7cadecf039729044c81460722140446694e48bcdd5b7be6c790749ed8530415277b2c396de6b0868caa3ea2b2d914b27fb799973f71f54828df6574ea105d2a680ade35a51a034e9e343575801f6f52d31e701f702f01781f875507feffe83e33b07cbad38c9cf0f5a2ecb315e7fbf747301e47331efde22ed5db0e03b1dd13237652e6a9d2a639cfe3efeb53916ea30fed9534f67f32f8cb452ab5e7eb5ea82747686f80352f59d77a2eba3d28b40ba09fb549a3cf7f32d7442403e8f66e7c38e5e8d7c6df0ca901660d1e858713e50063bb5d5d2988905e0facd073b3b5f767878079531274c2c16df25f81520257dd08c848707713b7026268525554f0ba0f8d1b662222a20f8ae03ecef73def027cd31e5d361fdfea54c5a8cdfd6aba92a0d78400a118cc9d35ef5d9f1e23b48c00283743ca3cf6a7196d3ca68f51826371b10b4b284f02bb6b3ffc628cc2438cd7bf65ea83cca745052a5a0036eb5e9458c4b6cf6897f2cb2a30431738657615407e30487df3293f0926465231c9bed3c4d15bf6d352cd22ee419b2b68fe07d7f1b2c285759225f3b85a3c987e5b6f65097759b461934616c1cba6d4b9399b42f81494f9c25f06f7ad3584efa8b7e9e31c021a9e8a696c54067f2240ce0f0d9ac97681e3b85e18ea29d84c942686cdb940a664ff1f0f5716cfbb0d4d18e0be5c4fed1613b5b1d129df9b054adec16f049e77b33f2287c9313fc4722e18561197b02cc0d9769a456c320afa26938afaeab6bd32d61ab5197bef60da9cc5f6305e4b66fe1496ceb833d2561762ae2a6111bef2b32cf4eb92e030e53c2ade3a1a6402dd846bb00bb0fbe0eda01bbd35ef602eea30d303d1a81fbf9ee3bd1ce9f8d37bc5f77b0fb758730eace6558b9efc516b433bf7d4db69ed1ffec837640e74be74dfbd8d4057d7851699210379ffdb141a3714b42f96f2cf8c5520cf4f214a6fc00d29f70fa4bc55f664f055d96f0f58e5743cb55db607c48b5eaaad33e72776fef3b00074f4d44143c4831270f4739e816bb3b42785749527987ad42a7b36f47a5fec3b3f9c9f7f066fd4d266b20d9683c16914d826d402ce6e4b8b2942f1c3efd34993ecde62c887db837bef860defa29eaff0618dc66291049cab56847de18cb560b824d504253b6cf70ca0fd69ca299df79919f83ffe63ca7352a18823fec5d0192b9faf3c6d66f900fcfffce6cfef19393c51f5a5d5b48a4e2333789db3f208dd6dc2d2fd02ab8bd83f4756dba2d99c9a3d1d67d9d4d3e2045bbdd682f7fa106b7f00a260b5f0394c3a4def91d10945bfe887f91afcc3f681eb5e96cefb88c9c6f673ff72f581f596e3b5f9f7262c74eded8387dbb11257124b131d197e74cff63fcadf8a641556478f352f5dd708b993ec1b4c1b63db182358cb2182715f782943752a98de0594e95fd9d6f9b9edffaa50156de88e156e603f215db566cecd61fa4c7ceb77092cdcf43872c5c58751e9c1c9e1c3d9a2f4b41ecfd65a3609dc16ff6d8638f2d04e882bd02eb5be017765de9d6c65078c29fbcd1152ec19c2833f42bab5d66958af69195691d0acece3b77d817782f50bdf86789d7d584f6ce0b7604681d654e782047ec3fdcd25f9ac33e174e4de1ab94b39b2c31bc6f371a775753e9fb4a2e57c3b3d908cc12b0bd5d5b0a8cc05c22ac2fe2f41b2e681f6ddde3c926fad5200bdc755f6cdcf2bde9bdd0b4157cd8158ed3a43d6a705e5cc5102b68666c7b84b1edcfaad06d3b215a37f3f092d6e79cdbda297b6ac380606d7ea4f79d9e2c5ea879702876cd79d3efac808c6bc183779db615753f03f571eb8bed550876fff8f8f8a547e7f31f06367c309a4142b357db81cdd10b5bf23aa1e80fc119c0d4fe04e85ac8cefc2f008e7e8d747c80b7648a0398656680943316477a6d8dc064025b278b0fecb2c79dfd33a4af2d25fb022c5f6dfc11f97e86dcdf663ecbb0b885e084f23b94698025c885f824a282fcd3d69dd3bfc8e19b6cf3d93d4c1bfae82c2e5242b4fa13e2d2a7218404f949f81d0ec9d899fd74a876056d3a9903c6013d2c04cff6a3b5bde901e9b5904ad5d7fc06487f12defe78c810d98127ca96bc0ab31ac722b371c2d5c81a3948806498f6839c53fd45cb800f97f64d58f5dbe0a0df5e70c61580965f55b80b5a6224e86a67dc7711bb75b32fe21e0b67e03c70cff78c152ce7b8076d298fadc5691eadeb52368ac6f47aef08b9b7bd19a7a7fa20c3e308ebbbd367284c794dbfb460dc6d4169a871559f154b0de313f83e7eeaed9bf8b684719086df119edf3c72cdce11bd0c7a07f6b9b4ca0f4bd7c13a256ccd3d506039413125c98a70a7f14e7e20b2dce04f78c8a445585bbbadc2c1aab28174ef59d357f0b9f6766125cd1b2070acc8bc102e40733434f2472552164e61424b93375c32837f8d9350c19cd525d4d578711e6048e1dbdbb5a4c0b8782bf9287b248861e52ff07e9ebfc2df28bb16124f3cf1c4ecf6eddb3fcafcf4267be034ff055f32d8891f9d383357630583d5cc45eb77775e587f86befdc55f3d754e3db9a98289af4ad7d519b6a1e8b4be28aa3237cce315f5c6da80c08cecd823606f7bd5e7f38f43aacfd8c268b9410401d7354a112e38d064c2f71416c73027156e0084c3d3d9f18733200645b5e9c8f8860eb0c2900e261c302d298320f6e9e92777f905ab5410cc5343620fc182bf7f8f7a5eb47801490652f2dbcee80ab15b9c753688632d6935e968cbd703dcde6d648ea44c3bab7c535aae0971604fe77de2578f8a46f3a30018bfbd3b731ce3d48b7a71751ca35e513ac21ada5fa44d37ad3c923fbaafbb41b3f88390fdad4d52e40d2e792f43b95c89fbf126691a97e5139d5f30d4f4dca79f414cff07e9e8c6f4cbe809683d6e1f315b0e20fd2e20dde5ba9f978bd501e9979561fc04d27107acaf659af37ac5b36173c53d9ffcd44b53cfdf47cfe37e94e77b5189f77f926feb4066a38d65047210924ca689c15e481873ecccde75925f2c35cbda87a5fdcb52dbddb5eabb750a4c2f69dab75efead7d11051cef1a1b321275794d20d9f251238c72b42c9e36c5c345651a77810a692deb76cb5286882895dad8d13b851d44b040874878a75b8233a730ab0cb3f9f99dff225e3c3b71a340d914b0756c2900050a48967c942d7146806ef879d7fd444801b2d7d1cd9bff1e3ded5b9c88daccd3c73be7292e43fdee3076ba247ebf5d6cee3bcc4d9fbfe8b93c65d0693947355e9490b8918e95f452405009ef131b989379fd08d0f37e88f609278dbc1accf4e196145fc712d307b48a6b8aeb96d66739dd991f66e261701350b589a9b12877f2776577631c810d645be3ec23bcc3a1943b80e083f368f8470cac7f97767c67d89b1b855859ea4f8d61746959d3fa8e5ad498d80ed3a77d646b93a8029236588a097a914e9e9af83f0750ff994683a6ed174d077d0f76216c06f557ac4d17ac5beb75367e40ca2fc2bec74edbd8d7f8d21948075776086bbdba934a109a251bb6e977ffaa337080ef25da7441ba668e08f202c7b35f1b3867ffdb08d21980c25756ec1b6554905e7c5b07e92780f383e2ac3629f3a34bd6b5a28faf12ceda3cf31ff116e9c33cdf7b7d449fd2abf535ddc82ac529cd459ab81574d312e81f35bf7777f7e4d7178bf9176d27af0e0f0edd028356fde6cd237f227dda9ae587a55bad7ac877ef370435f21b796ce31f5babc29f26d90ab81c842384baf06c0a9216b6b14271a143e515cd56a30e896b960bed5b9fc9ba551aa24d172098480d5feb1fd0bf5658e7d0d971c571e39ce81efc95d86debcb264de9c579b7b1d78d0205d25f897d3fd1ead9679f7d1b23d947c5554e374152ba7befb2ad6d4aa2bf31feb933a3cd577ddc3b5dfc73f2dcbaf49944ea76af7595fa90f1bc7ee7b87c5f9bd2da0e80d0616c76f3ce9dff11e23ca3bb11d8412b51dcdb442f2010b83772eaaef1ac038369d24921014c618fd97108b044c102768d808aa8a77993f19cfe06d2d55df0a2b10eaf35a21b34e23f43a2e7f5cae8d6027d785256ca6b7599c6d19884a9c7d79b3e4f7f9d997d536914e166277f33ad8c944d78ccce29e0688733819688aa3097c8acd09961a54d071baa4c3fa34d77cb4bca6cb74660dcc59721eeda3afd80342c8502c5155d9dbbe26ff825d7098179d1b6776ac53fdbfd24a0f6370d820f67403547dbfb267bc5887d04e94cc04d9bee00a046bd6bd20ba49b6974eb1fcdb93feddec179d20ad22f397d662cb3dccad97cb1a09fce9e6904f0f93b5522bf4d961bcde86ca1938361ba47406340fd62e7a993d3ddd50f4bb322413f0158dffe626951fc4bb5791357839db21a96f4c5927ccba51c4798916b12f07fa129907eaea0ade6de6ad4a1873475d0f0dff90047eb238df6925d36656ec3ceb0cf0073c1fcbf4ae42bf80a7c5d21e936c935a68072e2e397bc5cd5364f698edf28db3600d2bf99a9e87fe3edee5bf5a7bff5394a4591bdaccd58de711b074e732c34c41460ad7f62de4d064835cde7ccd5074d91b629650b2bfaada7b86c985d4fffbafb0184e3d617eb0f58444be88edaf7760494e00c6601ba2622162ae6b505b65b43d4608acd1b9037b53aecc686f6c169c89e0a8299db68986f044c0d6feee0593b45a563a8fe7370682d38e2a75d64f913daf0d332db7fd9dac13e8c6efe685a491863251ae3e04c9843c2d62a7c0a4e4fa3d598d7355f3e2c0dc4b885c233b8376e7929c06ec26e2ed1b2cc66b76edd12ac87ee2340d75d855c57db0f48a1fad70ab8bbb8c0afd685e55b312cf847fe19185e35fe23087776774e7ecc946abdb4d70d9f404c6fb2d58ab167fbb85e60a3153f72eb5b3749771130af8465bb9adb64588c1e4faf6048a012ffecb54bd8f95a3a858306dd65cdf9a12e9b14240dec094d9eed074dce0db6374a8746a2d0cc0013f0834ed0e7cfe28ae16b7ed6014dfb5761daac73cefc62e918bf759f4f81363e49ea9253e45879c5c806833519b3a240e8a3671878c950709ea0b522a7fb56a3dee43f43050457f9e47d1a5cd241b899403726f399c06152a3b4f08df71a3836443efffcf3e16401a702611b926e83b61458a1c0a8092eb0793fd936b6e4591b80fe6f3df77f3ffb1b74a1bf8b12f42b32b6a54f811c096c5dcbf1cf7e460fc3e23f9791827817ca7e77c8fd9fad10e342cf4d36639c9c51bc5596918e15a66ddd0f8429c0ae4d831d50dc803f7ff4f0f01f32f97f9d8f920924046d242d106e9c21d1504b6006b4766a4a28de2623b9635c03bba18ce0cb9a0c9361e0888fefcce73f8e7f05500120f605e7ebb684358cacbfc042e12b6c419bedba008044d48ed8b6dcad3bf5b5f626c234d6ad4754e3e01dbbe52104509f7b8a27d5779f9c9efe9ab165005dfbacecee965de13c93209d1f663d5f9bde41ba5902e80b9c173faaaceb68e703d2d3d3df853facc67d11ddb65ea5734310b9225f61296c83e7fc45cc0c88d1b1f81180d17fab17195ad13b52ce3eb2368174d3204fb7a34ddf5fa04ddf6fda74231e6d7bd22f02e96d0bcc4d068901d9d787a41441fdaf4829875cadf4079b5386f6266e3edffd219efe1b0c9712f635c53854b203e00fad205006cdf80ceb51d8843f33dfddff2664d95fbfcbdb04b7c09886cf75f8e18a8363ea3b611d73c27698855b608c2bf0b73d51446a9c6f7ef6679f86231969b0da98220774f5977de15db6bc10b1db35ea10f927e63b273f717232cf04843c4c32cbaa32f27a7494ef5ec22b7873d7ad5bc81bbf38bd7bc21893b1453e6d79349bfd0686f348bf363c508219265a4f707ce96efa4b1b4664595338b14de919144edf4b92b5d7eba777774ef68f677bc776b3bb3bc73bc72ea07db53a3f99670905cfd26fccbab8b1385a8e104b9771afa7b1e6bc28ec959e3ef2c8ad77bce31d673eda7b3ddbf4a0d6f5f9cf7f9e8f24fdb8ff15187ebd3de631ef83c79f759f3d367b99f0c7e22636eea4ceada568b9cc3efa97a95e5bd77cbef837f895ef3fc37cfa4e8e3f7ae7e97cf155603bfa9758d0b7ba2a70ed592246fb17e09b0ed7e6a7d6b6604723ed89ce51bacc3f9bf11de2e99f6ba9cedef9c6ebf804a5177316e721ec39de650c248fe7b69ff011ab4749669f7c2d243681f57327d8b355de57219980011eb3dd93931f3c99ed7c9da0ba0d66c28046ee36f9373f84219ca14d8640605ff1f60f4b496dde9ecb4288b7080143c0ba8f7eb27891e3eb3e0643a159cd436d6104f113b06e9b8d01f0d6cee9c947d933fe01aa482db62ecd70f6d3cd95da5bb5d1fa2b3c897190b6c9f171ef1368037eb43ccfcde90baa1f7767cf60ff1a49570c3292f669bbca2943e0a44d47e8269598bdf9a54ad4ecd05b905e00bd00fb6ab2ebe5e3f5d70fc2a6b7cabb48991bae31f2c6155863155d1cb6d55a2a9d3bcc0fcf3f73323ffd3bbbc73b6882fd28b34dbd454564b6042d41c8974074e6292f02d2bbfb778ff649918fc9495120ddce5f659c3dab35807e65457f4059478074cb341ff5dc136047ae9a1cb982406377d8edec4dbe838ce16753d58f21c45f87e0bfa90f70bd7b7699b613484788564b9e501502b7fea9acf361e9c9ddff9016fe2c65d8c6e9395fe4c3d20384dbb6bb7bc8e31a3505d6b70030e4b8e426a531d25c19757c625c297ec10882b8112f57f82e23726ef82cbf4735b163594f697053f032f83c572daace8bbf0ee11e6ee41be15d060e95326aed4273dc523ef4c6edb17141ebd29f3ce47817f3c6ff2a8d6493e97c339b6353e78d57197ae6021341bada40e3a98f93be545ef9b77f3c9f1db7c98f72eeb6031b2cce3c0e5189c3d6690e0b701eb27d71fbf6da3835916d1191799430cbef4f918a75fbb4ed83bd565e52b107ff98f1709ffa8e7d160a3c3d3efe095cdf5540a66cc2b6e6120a3046b375f0f403a1378c94eab9e16e5803af0cce7fe3926c0e2ff61b98658947f43ee9b4f9cd98037fc6e36876c8507bb47f90fcce470119964b59fc660d2f664f79bdcfaf40c2e73dc2f8f272b6a72c8859e42b7f96d964d1309ad1653f75655e35de76f347c11eb091d6d90730b6d510c5d38adb5f8495b2fab7373628339b959b0ef954e653b1b92d9b7a0868cf68bb5ad9c990aa5ade38d39ed9cf5aec65c62971c3e87826db79329de73a93fa3e0c1841626f9ee49dbd74e7ce3370f069a92dbd7c6d2bd380333d591398367210866064407112726059a66a0c9e884e3ae2140a861d2460f7a70101c30703d3518cd19a5b0c4067050031c1096018f3e6ff13f667526255d85a1b4170287332ac3fdb1ff1cd03593b8e488cb682a30019da0a6be7b11bb5f3a3b6639361c130b54d08b3be37bdf278d20b20fd78388ed1a822e6acf850e9afabed07a40c70efe9e3441f21e44718148ca39c39088675cebe180710f91ce0be38fd11368f08be3de8e5cca219999878665eb59076f832a5eb62a7f6dd4d20dd742368af34955f7b7f7fffe8844bb02e505f1c74bbfce7d9433a94d768b0b96e712d0e78a17370728786e6bc581b0c5843a9f087f4c90f5b675b34375b590fcd1ad970d7a01a2a29f9a19954cbf1a6b3ddbf8c1276fac5527ec60078ee5ef547f7f92d8d151afa61a9a58f205db7615bb38102c86a081e1925be66a0843bf6b4c934232d5e35ea4635619ed6f9ab05cbff51685763cff8b6fc91244abb6347832611e20c228d25e9328cc8bed048dff1ebd30c44e4137ca408cbc0649e8059b2cfcb50986656bc6dde0913f518068f9d261df24da0e9726079622b4112b1bd9c96a815612ecb77f66d23ddd4d70937a5f9322e6600c44d6040ba03816ee3b94ce8230538b510025b5deb5b2a4c2ec0196ddd5b3350200c6bb495be01e7216724a22794472519cbb4febe46f049e400b4225fc224b3e9509e909c9e3500dcf04a930c56d6c60bf96d1a834d94dcb8cd6ec191dc8ee71a8e6bb22312d2d51461baf1d91064c9a2c46aedaf95d164ab89afb5a73a1cb635b2645d38ed4729410f992c374978a0b893912c0924899d2045ecdea1affc3d63ce334735db3006821f3348a2415f51c08d794bb6c730dda1cd7ae0fdea1724aeb52dc7357298f9fb17a7bb771494687c48942fe319b45c4d45aa105485b00d31217f8a92de32275c94e971f430526730dc397d9e147fdfa4cd34fc04e143f075db3408b6af144b637dcc79e81f4a5fb1c22c126c8ff5510712eb769c2c301d9d5aa332802978fe204e0ba35c9ea77df59f4404602b70b3d34f01b4f2712d85aac9e79ce9e5555a750978d81710a6d3d0d691aed5e616d9c6d61580be810f95f6dad8b0f063ca5a164ce14f939d922c2728272d43db080683f12ce3679f6290f9c746a3015e0198f5f128654c1d1a190b5f4a9b9ee318cdcc969702e0232807d34fafb22bfc4992f7b453b916a111ac0bdad94c7324e82ed0ce3605c0b700bc83f180f21ed6dd7c23437a2ed216d0df3bd93be2b18e04eda7846befed1efc77d0e433e98f569aae89fc4ea2aca35f0cbefe6534af78d213f6141f31ae7d586a613cc3091f965ef08ba523606f39b6f7910299e8946985564d137477826e40adf90550594a31d8d650e57486ee6c2c6ae92e8dfa32e442d756a3ce801bd0ed48e17cc51f8c1043c818291d70225f9830f4cb939c0293ee9290a4b57f8589c9cb2dfd8720d393ccb2e5b9ae94cd3c94b3da05d1966fbdd6e9fc443a839234d96d536bd7347719ee2446e1166bebf435f09eac0969f5128111dab7a7b02ed2287f86a4325ded8765accb0402f27550be490b59695a2ddb7b64417a872fda8d9e72c0df98c9d60ff96b0268cf3dbc57091879914fc4fb733f266bdc34777325bd65e967ecb078c5afd2e96af9085160e46f2e85b1f1bb2920db52c139d336aaf16e65584395a69cf63a6c9c0d536e084bf32d2fa95bd7007b650a4e09e63336c0ce3c168a4d190bea342ef55a567fe65694091de91a1e6be5e717e8ffbf5ed51945ade16c2d6b78ea8ae3e02659b69c464b5d0f8801248e1f97ca65b4ea2f3d3b9f2d3e2e811310c629180e75f250a9c1273389d3eb2583656cb34d6d1ed2c87bc3c32cd8750a28ebda7400ccfe7891ea8c416026b00b402ef34b7c59fc4c1bf82c9adaaca4f19f5a0d4b63c2155dc60b067d86f648b4ce34ade7189a72926d67f723233037fb68c669b45039402a6dab746ad3750fda74c919921a2e4097febaafb3f9d0873ef81fc3b7fc0269e810663556661222301d1e3b9d1e12bab852c232c9ce4efd28f96f985755baf668c68f470d17a46b740ba00ba48f9f5216181f01bae9f56b1b5f9b3b07b07e06b09bf604b05ea01d0189c65d206f78bb085b71ebdf7c2152b7bc6cb769a0c1f76790436823b72ea435a1a183b3fdadf5441364314464a2830ced379c773b9b7d4df271dbbde287a50502b75adba2dcaaadcc3ae64571d0692ee5a5bd729be14ad638d13a36b51864d21435aaac96996d50ab4117fab6bc69e4093095bef91748111e06c441d770ec37c85185bfd88e37a4cbcdb878cc9842939db128db06921ede869ff64501b35909a09ff9cbc4ca032e3a1857400c09e8ae29add797fc8630279d60474eac8d629ccf921571b11a4bd398dfd26d4fba340ecb49aca265fc74b3053eab094e7d73b65b97659511e08ce07cd44c8ee195fe3ada6de1d7e90c01da2f5837dacbebc8874ce88ec00c01336139e109fa47deb0db9626cbd2edd5f2854d389ded0ad2e3c5d7f81701d24fc2e032e6c5f0ba2ff04ca6ec19d7c61c0b6ee9d32ce52eb2e7dce07cd8c0b5ed8bc8506ee6581b82f0999be651a80ecbd1f24600ffbaf36d1941b55839e5991bc6b24cea712193022803d35f8e334f9dfef72d6479171b2e7d4bd7557f9f7994db65eea2da18729fbb0b2c6af7a636adfac9c98fa0b67b31030d448785216db411f177104f4c132ef924f79a696e19084b227861a769fe1548ea9305ce2bfd797607e901552213c1b017c0e716a2fba33625752998ca037f364301d264e06bb7a48ba845305bbea44bb3c949b8c2cb80fc49ea7dae9570f64e33fcc9ca987bd4a6274fd15ccf40f7c45db79b1f90b2e0fa1b32ca014159928b6d75b7a4c6347891261ddf74e11bb79d9d8fc3b3e704e98bbdd50f9397252c5d9ef2e2f6817d3e209d42d9f7525b5e9e78e228e105ca4de3c79495b6c0bb60bd00fd0167acdf3cc839eb82f5ba2acbab6e0bd2fbf5ebc8ec27a55f1b9b258a34e9e49194f833509b26710ca564ca47d3393f1af7cee2afb09d660fb99fdf615ffa6eb6c0b8c8700bcca167464ffbeceb47900a0416607fd51ff2012f10d23756486b689ffd9b7926fd46353e8543f20bd976426ee6d559bf6f79d3a8d99436b845ba7414eeddb411a7c088db6362e447f1c81ea426b4679a0093699c7432f75489d1595384804cb7e15ca42d9e6721e67e18c2adaeaab498cc5f8a40cb92325c20a46e6c81786bb1adb06ccb3543cb446b52462fc21a5a3a273a2acaf3259bb71dbaf6e16e5d6f7ef39bf7bcd6817b01f3023dda64bede06fe2945f240d26a6a9157d2655f36aa69b1c352133954cba4c89718d652327613a15b93b9d0b94e36254f8b09eb100641752f86fa5308b681a44b2476f2f67187a8c8068182f0263b24d04f152a73ac4bd09cb9c1d0544efed6885e9f397d76c72a93e8b72eeb06ebd14f5a2c91b4a7b5c17a5a8d5224c7669336f9f5d31e127e881cbfcf15233e14a133ef6c547ea913ed492fb494dd4df26add0fb2b1fd19a9d032fed1eeceee87a5663eb2c1a16048706e047a9739092c9e868106b57d726d1492cf42040b86711f350746cde719ed6762fa6d134827ecc8cb243b7b7b9f6210fbb492af9006684732accfb6e59e4a2326bdb93427ed3795cf11bb3d90f73b08e64f516e8e632cdb541799ad36fd22ea9c1fc764f683f4d39cf222eb7c5d16a6b8b2374079e30a9b528c7c56ae3aeb4e773ec740e54751dd6c5c805764b4e9be35dbcf99e9fd94976c4e5f6e7999cdde3269ce05e89ffdec678f9f7beeb9c59bdffcd9e375c06ec182f505d793a8d805eb07370f487706b417787fb5ece999f812fe07a0cf8b7ebc661f73104d9f8346cdd768b8dcbb4a6ad34a46092b8d673bef42e4f9b054efcebc7d3c226196ef1904ebc8798e6b2cb06e7a4d81f6e6dbde1b05da64197e28b0182760e7b44c6e4a75febd11083fd02ac19b96fbbcfb15dffa26fb962f8d9cf220405890d2818ca0370c488fc1d53a4f78242fd22d48119ee061d308890049b2cb3927e929d9848407b450685b1400572cc3be48272b9ec7417ab3385f096a221ab2dd7209b159bd89ad0cc2350dd098a7d5df83c9938cc9d7722724feb658e099d33edbdfc6d73c13d9fc387efd1a81bba0ddba053c23e829f06edcb534a14aa3b35d3646c6291ff257a70b23ed447a93e95c04c80ff05578a94c38c3659b71cbd8f96516635afa02ef26519633ae6400c76fb10a99a97b5cc611e3db7fab3b6d6869955bcba0b2268fe6765240f86ae19ab9967041b8ed14889bc7c7cf374e7602ebb50c9ec91678699a8fca091043ba004818b674528efd356dd4419f3a59cc3e611e0da730b4c3b9d7a6720f5232fe3290ce1b6e8b8ed904d28d08fb5a9207e75e1a5eec1c2d45cb17b73857fde8eedd8fc097cfedfaca43624b53e92ea171c72bcd098e76ce602820fbc310a911265220c08b623e03057f85d0988ed4f7b0cf03ec2b9a7404ebcc4a9e2a3f60fb1ca06c8ffbc33230da061b66581a4ebc83740b60bceaac6a01491be78caf8ee7f3cf27e37013b00fdeec20a53dc734686a63b5af6f7949f89027c253b4365cf7107fed9c7e400ae3dea3002de50b32e1c96b446c272f4d5ea745ae5a1f94a7e1d7fcf4fdacbe6f73a14d57942e36a54dcf179924cd99e94b2c1ad05d9a7441f96300f4affe6a1272fdceefcc66cf3df6dce2b303602fedbab50ad69f5808da9ba69efc01ec82f6a5dbb02ffda2ba09f023445f801a1f2c3af61e69935a1fc8602eb51a6d1da5d4ea39894f7d24694fd73e2cf56cf547d1aa9f44ab6e7965c60f4b0ddb6a6e8b326b36b46e6f140987d88e8d6d9ce1ee7fc65265195e30e96967d65d2b66f4f232e8ca66cb1748050fec02d23dfb79917db9906ee138e218236fe487b74008c2711a15de982ec02b89c33747a206caed47ed657ecb6ef946365e93535f0370cd99f82c1c8c33b1728265b4b9aa5f2634495262624d6bd949d333a526d2b5b6f63853d36ee1a0db73b25f3e253490a85c02cae7e345f404de750bda0baceb2f805eda75c3aea3e92f447874b9dbfe6485eb20a16c78a8df1489204de74f93b1f474196411fe875759009a2158c58c1ad38a5d0cc7cb65fa96a70545240828b0dee223154998222d0ad3da417b94f9f29b379e3c40eaca62917ad580e75b09e393a3f590c821fec87ca9a59da400002e63494441540f8f201b963f5327138ed6e8f412dcb510a0099fa1cddf6fc94b0342e7ff8c2a7dfa9274995297f3e96ac8e53ee9f5409a1140f20041437feaf8f825b40f3fa66c2850b2350301b42f0644012a2f92030776adeacd2683028f4db3bbfb5191d44855dd5e2e9ec605942098a018b5d505822b4c30ac0140ff53b8fe4b0a156e9cb45221477ad256324400f5250d4db22dac409a8d136f80e0ce2987d2cdfe0ed7b9c66d2feb91b66f3d4cffdadef409985f77805eb4dadd597ccc814db96a0b2d5c08947ccc47be1991e0290cf27d8c6ccb5a1e463af1f0c30a9f6623e72f5b1e3c98407a7d3c8a0c8c2275469b8ef81c3d2a48ef1f90b66d2c5fb0b86c75518b0e369f79b51b781dcf57f327801f35ec05d8cb5e08d8d947deecd17dc5536092f7fcb423d8a7759ea3fd9310ea3336b5ad96ed0bb833dae29088ad23404cfa46fa47b3216de2a0fa53b393766e34f11c3ded761709e41618c1fa51b6c040ea6c83d96ad5439a736fca75c64e52649f28348f1f3e64ec61d4511316b04e9a9ca31e09af5c1b8abe17753ad9b71a75c4bd03f3a90ba87882c4e10676c04def226150c8cf8843bf687c923706daa17a376a4e92b7f1aa34e1c4b64cf2953ff99bfa09663ca0abe1b0286ead6c3d4d2e2c32d3680ba25dcd11cda5a59980a0289eb4cd47a09a7b23d492b72ca6251c8f25d7372a6a35a30f256ee1fc47192cb8f7c66b04eda56917acbb25c65a345bb08e3ca0e80bd680beda722a503a7c811b848929827d880d4e326d2828b770c91ff9e45fc604f8866c268d9a6a0132e9bd932cb61ccd98de52f5b89ec7ba33c69b4b63d93d9ff3acf18438de643141f9898f901a4e3c59f33cb89d71fd0577f7945b6c9e9704be31686f0daa7d7902e26d5b2fc3922d90321218db32a4886146f32bf3f3d9f721a7b76aa39fdaf4bb40f4bb77f767fb5ca339e91a754f3f83fe5172d6892fe319eae6294d7ac9ea588eee4998d723ee773fe071faa85437ed55ab6eb33f0e49bf0b9b43ed25bc0c95718a0c369af2f665bb31fc0524eb22d6e409c3c78fab50ce3f35a48854485cbfe817e28f2038d10004f7a49b2d4670de9d95ddc1ed030c3d5f6f6f5004f38a0559506e08e0b23d0a46cf49060121ad5f4e94a63fddf96922fea4a75ab1e850537d4674ffd446fc119cdebe95b424a7c666463a57d875b43ff4413e20ed3f92209394294fe3115742dd0c2a52cd31a405310264c26d72179a9d2e3e60ecfa99e9f5f1280340c94a403a6036c731ca0c3f204d64443ca571fb025b5e9ecc8ffb08d251a2cfde7dce5b8fa79f6e1f013f8686fdb1c71a7f5f7ef9ed3432939f5fab6e5cbc19ff6a98b14f00da2d926329ef7e1fbde8e78b9e1998334047fa15dac8bc03aa3da587661ccd4461d8eecebbe7a7b39fa3f1bf65a160750ea8a92f1bfd8d8f861405eb4cf039575d30b8d5dc4aad3513c1354c7a2bbe4c7d6dc6c676a482be4e824ab883550628ecbc467238993e0bc0ddcdbda8d3c9b2e58be20d4d45b380ab483ee388e42e802ed905b9153681270232e791d622aac3c499ce437f61dcca96b2f0b09593fa2c5fce52adfc6db34d02539061966759f25b8d65b6fd11e6dc6a5c8c09e2cf6cd503cd8d9f70cbb5bd664816da63caca8e0723284702492778372ecf3ce3e82816e00e812ec7f9466776e7a8f56f150182760190805d258660fd8b5ffce234b709820a1091fd7a193baf7d19422a238dde250be16e686d841c097fe04d084f6071493ec4109522f127b6274d182166f5e6b851796ad1a56847764992e1a5b7a785ab0c80ffa6e14ffedb9cc87564a5d76bf91ae2db5b22022c472be3d1b2de0a6fc95bc636bef9304a1f17c1ce31561cbc9870c3b254b5a26750c67deff1ec38a7bc64b4f3c86b7e696f5fa04e0a64739abffb0f1d65dbcbcece66c5a8ed19cd45f2697b1f4803801c4f2009ddfb83b0277cf1579d561c1004538d1ddd8611f2de23b0dc760203c2a89657bfe2a140ef7c64172431f5f25eb8109cb09560ea89bf83f433daf496a5178085a6e2ff989d9cfa338019b81492080c8e081afeb4acb73d2b3f1add40606b23e3e4e776e6391ec8cc979aa072520158560059fd54d9569b7e3e09f30ba43b7c400a2a770cc86483d428746d20e98385f18e404a14034d3abf5a800c1cb38fb328fcbd7c40ba18b725f1c3116b9a745b3281f4fc0269ff883420bded4d772212a49b5690ae5d20fde97e32cf6403d2dffdeef71d7999ae8c3f06e4a5a69d49ad5fa3bbc2eedd66e2e4b4c676599f0b815a0ca8b9376c7777ff57e97d7c582aadec0df40dadbe28dd75e4c634eda05dd23e2c7d032dfa1c62bfd8e1c3d279fbb0f40e1f96eeba056655abae5f535af511ac6fb5b88d365236b29bb1464a43e7f0031a3b6886454dd6fb681af96fc709b632d6eff7a850df6ad42160a43ec38834771e93f4fd4f3e087e082c00d4d8e2bc45ce1e9ed4a665d2489f4abf31daf98f583325b9e3138e4c425464bd0c6cd3b8d619ea102620325bbbec83ba3466329e5b222d9f36e26e6d74a4d418695db65f3f370ace9cd77c84b4bfa421bde0c910ffab9406d205ebfc1c7bde9c7104159a764b14ac6b0bd6b56b1bcc0882ced35a9afea135104f7e214d9056e27b75fe61290f5ef225d1f228a9025f5bca00fdc6f5dc9de48c314377ea0f4e510e099677798b823b4a2d0bd5d1fe7bb98de7ad6ee5c6c69a8f424917093052d3edb4ce34bd50c175cac43665eec96bf328cf789b9a1ac9e7f3d55b06c295ad94a98b722d23a953d8ee2709fcd602e900a92656d9c678c25ed4b581e668d303d58de9a629a3f05caca958df9f7e9e6cdabe07d61458c79ef6aafb302fbf7cfb1798649e9127015511303c1a19192e74ee240c1619ed40441c82f66918f92f2634de15e4b1ba7b9336dda218402730346aab05c2e3c59ef3f733f0dd49b566b4b1b6cb66712934598d36b941a808193a03711f671ffdf0034c2964e5562b3cdae4a3443beb62a2da68fbeac78d868cd612a336bddcd7d986d63f885cbc55fa8705f049fec8bbe6e24e589b940842de1c109433070458f8fc7c77feb74cdd84637c45e6afbb2d35e9a6f134466d410e6fd6da718c80f43ae5c5b8328261ddf6856e1f08d8e59db69af402e8e52ed0aef6b26930d5c59719dd1576ef762d02da42a0017ddb5a80bdc0fa7c67fe0310ea8e43a314b50b4c338603b14486805a020e53b5c036e486c83ba77c587afa6eb322dbf3d94aaf50ab2e4d971f9626a0df46c03e865f47b73496fe2e2ca1a3aefc35de104782a634202eff3244386fce3654f001d5a45522f09ecd56a32ec9a27be42e0fe482b7483ee46e3c11c0a707345664fc4932c728d982272c7432d14dea8c4fa2a8c492503e5bbac048636713c070e52f59bd918a6cad2f9ac6a056bfdc0f984eda24a5d806e22d3d260da2fd542948b4ac16942748aad60ee28df0dffa4cd7a5cb41ceebc68dd9c10d846d81ddf416dc41ee87278701eb02f611acbb0da6c0ba6d1901bbfe6b63241ef46c6fef3bdf09c8023c8242347678463ac174381ca6397eca101908c775f227e7959df871e6d8cf544339f0d12a233329b52d189d0f2d3225588f6585e54d8022a3ca78e2dac8a3d3589b99161aa09f44d17e6367c70489d23fb44d607cecf6bc3e9f9299d26cb4ed5256ad94ab69ed932171c47c0ec7fb988f7f806d2c8c6b35c62d7f3cb2f63f9f9c2c775700e28f815993f276dcf6c21c785cdb5eac69dd9cf7c6a7c3cef5e40f8e5f802228e92d8e08d031673b2727df753adffb4748cca1421596c39b305c295018fa0092bc912853b1ef7b67f6a126607ddb4b47ec5a7bdd5d1482f91343466df53a4837fdad5bb704eb59e933803c0bc33e8830bcb7b58f04b62b16ed8d1c2980762e6ddad6050ac9fef4ce62ef7f5ea809d960aa4d12a5af1a5a1bd5a69f734a906d23790a1c40dfb4e8d850cdb508ca07a4a7b3f728196d8e6bb224a164911a74652aa1ca94939e23918b4342e51f93d30f43f717af7a1ce331bf564ae61cc7b858f4ed04a54622bc69d38fa72d2fa62d93fe00387f1fda7335ea82f2d8f493b8fbb68f02edaf07502d8d7581775e47ef09d8dd0ec320f6ffa039ff20afd1dfeb20dabb218f53bd221dc1ee1a82b70f1809b33f742638f8332d7c0f65fd120b802f7a5ce3215af5c5e2d02321ddabceafa42e3fe051ab6e5bc6671fdd45cbeb66676a76e292d6983671b74141f93742bb26c58c490c190b26bd39230d6f81a7c9ac95008062b5e9afd4cac5ab982d1fa492fd405a437b8713f8216f260580830aff4961ba006ef96638f02a1a44359bc4c9cf56a41982c9e3379422e55d5eef278fbcb5e870397c16289bdef6e46d6106c1145531969eb4c99c7e6b08b9c843a218fa652bd787b112d3610b00f3c16c1f472373e6b37f6335e96b6d72683d3d3de497d398c41821f76e1c1fa0c998a1a53b7a54edfae1a3fbcca959280ad60544a9bcdf46903ebac7340fafdbc1b2b144daca824666dc0a469faf4c13f04b58e3050991a96cb592fea62573f2ea333a053b172249f2b9836013354932856eefadaa76024b9301234c97a273ebf267b918e540b14b1afdba293b6de97e63bd4a965a0395ff7ad6080f79d302d236e3d3873204fbbc564f92cf31c6fd6ddaf88b9c00ccd1d6477c03c61bdaf9091ab63e1f27bb0ab7d24b744127a440ba7810d32ada70473e537581f38b647259fa86821e8420417ac00940646cef4b77ee3c0323ff7a70930c24320c36913c85813ebc0c8f5cc19df6aa65e7830c4acf99accc31bbd4454eae6ab08f0104c51d930454c914183db621e16ad14dd48170ec7223d47cf8ea718da6a05111229c4a7f021534e35a944e38fb3cc8fd0380f4b10d4933ded82395b3d3fbb9e9d9f2a2d4541b0f594990fed8f6557b5af1ad14e93a96775ddd70e2630e08911f65853fac889003479b7ef49b86103558991cf1878f3bfe5aec2f2ee9376ad397a1e542b622379cbbcf879dfb27b0e90855fac60f486bcb8bf26ffee299205d7f6d85293b6168d1b5732c4c73e45e60fab5b04b735fda52b7d994769d3e707274bcf808f4e3174ba199dd405b8ae2c9d819e2f7c6d201f466916d07d68321dd53e4fadee6aee31a13c5c0f968880e6d371ed7b805878d4e4ab7f46e838e728c1b61770c6a173c097f1a6f12ef049fe9c6e16275c880b7c782f48b5ffeb6baeb5e3252feeb698709e907d23d40c9d90ac552fc45143ca68c72400d3b1efb8ce3932c314e26fa17e344272fbd69e3358df99bd2caf07e19033a72fb99e94d0706138d9109bfd18632dee58f6cad346d8ae9696c54161d640816a44c93ba98d0f63b8856b8b9305968e8e81ad9b8f463ac9f2ff35058a2b4e47ebce7a158f31bb31b4df06eba15a669d693bedfd4aa8ffe8b00d198eee172cb63fbb7b847ca373bf2048fa24d265cbffccca20f829b56deb71557981e7f30534a812d26e02a90dcca2448862773ab4fa169b5b67a283a7264e911bb049805461bd7c395b130d03a7a624541796f895a4265ce8029ad724d98b52aaf999b755b19252989290f1f55dc414e3f4d99efa3ec3fbfbbbbf8793154a93a4f4e062d3ae997669ff9658907154be33a483f61cec97ceb02526dfa321f2f828663192f93491ff581370214c10a176fc5e47278b50b21e668d7ff4bb8f15e19a36e20428894f95a270f2fa80a7ff9b5c8d9ec8364663fd204ca03cef56ba072085d401da1d7ef6b8efca85181e0d2a6af8374d2da364d6449ed3a69ff3546ab8f23485f17a1418814aa3c0452539da0b7f1333cc7f7b3aa7bae1573f6dedbe4aa2e401e7fb60529384c9cd3a600817a6d7b19b4e90be978b6d4eb19e22f90d2ed3fd13a738404ded8ed871007399893498b184fd14cac932a69717f23d4fb6d2988dc289fe71ab7bcccd1a62b8802755442e15794e9d3492f47d99b2ed015a817bfb0a78fabcfade0758a28e07b916d53d4ac6b239b73b65bfcfb7ca8f8b423a6fdd23bcef6561e5bf9afb74a0ec56d8825653a861684c70dc9df8d12e3b70e0e4e59207b0a0dea74cc7c7ef3ae5a7517066af3a59f5a75e30a1c567b0dbb8ee6e77eee1f88a9a0a39399b46f3c901b047235dbf9378b5012b5d7dfa73fb1335ffc4403ea92bbe12681bad7ce8d9d13f671a9b0082fa0bda7fea8f0702253519071553e5c771e40a3d96f60a0f0d7ea8e769231a6c0158ed0de33aced231963604bfc614c779b304c242f7c2c0ecac566046d99651abf7b285585d38e721afd94a44804a8c3af80aa6826293432526993ce9c4670573e6c13fef4e714a96ce137dca8c468fb848ca2e4cb33a55e6e0aa456ea387d1af0f73e531720d25df3efeeeeed23cfafbd335f8222b5eabe81547b2930ba8af6d2321f36f3077ff007ef85171f68c095a72b81d0893b6f4b2a4cfe4880cefca025dd09d4c2e118a19d7fe292de2c914aca94f7cd9f343d6f9349139bbb9561d1013cca6b8575db7935bf9a4b9472d185cf356b24c606248d95609ab8287bf1f6ea447b0a5b9ba5234b0073cafd976c41fedf69eb3f6118fa17e466be65ece2d081a8d019aece07e8ad7c71561bedd4b82fb7bc88b72877c25b0700f53bc3b69775796ca59d7f7fe0b7bef86882950e546e6b139411e8291c50eac760c8ef30a0fce780de7739a1ab3d881009d2e53dc7e621233f49a667204876b73873740dfa641324d82a001c906e984c418832091548375c3382e01632dd778d03acff11f67fb43fdff94b4c7e7f91f1eb2ba2492459093de3e98bb4fd93f3ddd9dfe684905b08c046bed1864c7805d2d5a6db28dba789a0e3b78d3cc485da74693ab5f49a3a908d97e9f8dfbdece20e047d106b34c9b62a01a5438e62273c0450de6045cec4b6fb79681e909e6d2fa2f80bcc08d2059402f6a64c1f3f206da7b39436dde2baecdf37fc2ae03bda05c0465bcd7a81753eecfe5fa0e07f0a25b3a91cf70d35326869d086d34fe7d076717a10ed1f740e25217c7eef201fc3e16e63f563d2c423b136fd7c3dfd57adbac7bce50418d36aaa5dcd776defdf1decc6313a7ca07be8acd7482a3d98de4e776e48782743eefbaa48e1cf01a2ff3b3b1d9c67a233ba8cea7405f98aa664e68ac91fd664fceae1cecfe4e14ee103f2de249e19cadff740ee99c1b2e8678cda770ea3771cecd2019cf800c17347a3a0151c8e599a8c516d306b2512cc1c7322c87715953e65186e818dc395f2e01a985424685b20dc2a9340d2f88333fd63f9a4cf5e9d646cf5a51dd64c76db460edb6751bdbfb691d31adba2c4ca095393cf24edf61d17839643beff132d67e63edb3b8275bc6e5567bc9cb6c078646de643e3467399f6724cfbb0b899877e85f1f38f257cf87bba77089ec09c84c7d18a44566637426c624887ba185bb08401fe82adc24102dbd86011426e79bbcc974c4dc93821e6c6fd86b0cd057f95579c69466e841266fbacdd527b30382d52471dc4028694a7f6c396c26ec2f0fbc627532cf16af7cddb6add791909ffe39dc5fc859dd9c90b44bfc03cc06979f334b9e1a2aa0970ee87a2d9b2326e7349a3a65bd3b64bbd66d641ba78cb45ad466dfa6907e92df5bd69d3cdd33850b91f705bc0323c825442abfe085af55b52dc0efe35d08e0fcf667f5adef2f0ff2fa0ea5fc2a8dfaf7c00a4bc2627710604b7bd8069d3e10ba49b963cd154cb904d405d6d7a07e926df3460a47d46aa5927ed1c2dea230caaff0ee2f5679814ff34ee2fd2be67a9f79f514d16080c4e691f71699ff935b4277554ba0ed21346dcf4eba8a605a667a537b451799eb4b3a6d99af3295072f6f6b7cf768f8edeb6fbc20b2fcc9914e8bb277bc59ffd939383bb78e05d7a3b7285988010cf31c4479eda969745dbf2425ade08dd4513143eaeef4da71df70d383fe7b136061728d676bf38a07917d24cb4e2a0e447fc6127172977eecc0fa42132ecbccc8af37c1a5a9989e043e805ac3fe1175d27ad067168d5ef9cd1aa1738ac766d6cf4350a2cbebce52d6fe1e0ab0547052fe6b7f66eed1d1e1d66ec81142c989cb49d187739abfeb8cbf538fc3a6a1edf16a7c33b179dce6991d7ad465dca5d6e0494a6626ed8bb7dfbf6fcb1bb8fcd4fde14a01a3eecf28a8857438e3bf37de683d3fd9a130e36ce11638dfe94e2ce3173862933b3602757f3ef1c105a3fd87250c3cc2a7fc7f2ca5dc5b59f6a3c13da0a9f2aaaf85766dbd7bdcc0d0d20c5f12dd1ba80ddbe2e50a7bf6f35ea6be455aefcc0d62d41cc37f3bd975e9adf3a3cdcc35d1220463880861c69bbc30f4ca52ffb9b14e7ce5fcb2a14a62a06b7678bc7abbba79aaae9f153448bcf71e4957659f06be2426caab1572e9f3c695df58673407a3a8d20bd168ce7c9a2155fb6786c90ffca4dbc7f130a9e042e0378117c629e6f5603cbbf35bfbdfff7f6f6f67f0cb5da7f4d827f88f0fd5e406e170cfc71d1fb45e7ddca8434894ed253aa830300e38c365d005c9562672019fce50c382e8fb6009ab27f1d4cf20966c01fe2fa4982ff3113e58bd6a96028245efac7cbfc869bc60b2949bdb6cfb832d491e75807e9152f1dcbbdb55729b04e1b41fa6a8ac1b7b6c9e522a22273938cc0ebe5bef4a1b8279e70cbcbd90f48d7db3464b92f9deb60d8ed270c64390dc667b7d14cb477fd6127b69cae187a1b13735b34af44e071b43d33e2820ea1ed407a7f1fecacb14d8616603f9be27a84141d363eed1ae9187bc22b78c6c267fd137bb55b8b9c32a532bd2bd40bed09e027795fafebbaf3407a14405fa7cd6cf6420fb263dc5c39dc68226e4b9131def9a1ca18fb86ee6340ba899c0d9258f0140781385166ed03fcdbdfe93eeb002ffce75c7b847b19dfec3ddc5c47da86afdbe79795f42c3b2ca7da5fb61db5ae0a2b7bb1b8717093e5bd7f1799cb40d145791fe4b873e5eac9cd4f85760f907ec7fe7a2148a7afef2faff9aa7b5e7184a364cc35a5d74f7c85777bbe98efb346387b01aa3955aa87973d4727d6c263e3d61ec3267f850f69363ff9d95035e89e68455900ef65bc209db8096b113fed4b37553b732c5af52c180d5bdf9b6e586dc9d2bdc99c0f3436a5be8fc30aa00b5cca4d7317ac1c4f20cc0a589dbd1c2dcfd1dededd0a3fde39389a0635c1fafa558fde07bf33fbd27bfc5446d7a6f7c542b627641161dbc6f699cfb405ee0b48577d6e5aa9b241e753f90a8cc253b66ed33960936eda974efc9985442fafacb4b1da5476456eedb314783bf8af424b9b5efef36c9553feaad018ef9e7406b8db055cd4a68ff1a54d57935ee16e79f96a3cc5a7b22bfe7eb70b888d80bdf68adb76e8e1fef1e38075fc8787274786317987060dac372ddaf8ac0a7fc3191d1626af2926853aee1a36b31dcc1f409af868ca0b81aa09ae89a95371561e7749ba95e0f234b0dee022fc3a66b68eac77403fc92fbc0d9b185fb33ffdf1c7bf90f167948b2af3bada4edae781aaf9fcc53e863c9abe5134725eba7bb7e687f0217436dc8bdba4d8d15df9b445c35d7f539d08fb0c461eb34cee31d5b20fa6444bedabe73465ca739ea3cad2e6e71e67b4f34cc6b505c95a514c7bf9e9e6b515fe5aaa8be8bb96f4a1f4aa4d5f7db0cd48ddb761b5ca5e4dbfea132bad86bc46be1121973a12c4c398428507f96b35377fc94ae1eaf2976d5adfbc78b57cdd7fd2c25a5cc3596dcf3ab5f4c288b30fddbe08a48ffbd2ab7cb4ea19ef0a985f5516d71856c53d34768822587fe466b4dcc7770fd0216417abcff848b4e2ed690f33905df4e40e78c4af0c7295be00f6a0a9366a02c100aab078b04b5ed6cb63c06d5a6fea3b42651fad7daf276d6c6258352f6db7bb906723485fa6ca569bda9633b5cff86adb9876eb6e1490365ce1d973f0d56d2f17d36655a5becb47a282752c3ea2039ab7dbc47b41ba8014d6a78e476727d39697f11748ad33e7a33fc06f3e0aa497ed3395565db72660fd4d0d7b9f05ebd160dcee8365d2ab69d72f904f0037f7a96f324c2c1b371f1658dc94e7610f1b79312e9ceab91f397e64a22b61caa80a8000708179bbf6bacd692f243887fc5564b7bf72f28f6d9802afa16393c6f7c68d1b272f4f0bf94701160d8c1ecd8f50eacdc3873e3f8d141380d43586ebaef028d5199a62137817787c77072f387979114af9672ec1b4e16537f7314b06c2b89892e266a9d0dc1bcaa872ab0c6d2f1bb987663d361a796d4d844f4d267ffa5546edb12b6ef7b6c73416658cd96c36d17773ca8727747ce651a39b27ac17356b8fab72c4539b4020e9d76334fd3dfc287b5db1c9327c4dd979829fabc2277b0847d9b8db958f9ea47292f46398ee799493b01c5bf7e057e31de0ddb4dff30eb85bd808be97ee7a26fa5003ec0a179d81f279beee093a6f6eea53e6dadc433a36b49e7869a08189620e0e961f8f1a307ed0dc522cdf9c1568aff04df62560635396fb3b4c3035822a5a1b303a6e811981b05b4334fda9181f766e7369af5f02e0e5c0403ed28431bdbc7122b3b875109cb4d5be5e5f598b51ab5e81dad661fbd634ebb71df5aa8d7d04f4c3d1b4a19ea9da6739971dc7689aad399f0223df2e07e996739b935b4e02beab54c1ba7f9e93eeddf07d7e79d48b016fdaf2c2cf00609a0ab36d797932a7948c1f900ad69573533e68a600b1b6e0ccf61738842c4b9a0ddb2dd6c1ba79e816d2d07e79dba9bafb8de2b3a7d313f66052563bf5258197dcaa2d97247b28a38b27eb3460f289e6db87bec3e4b3f6f0135837bc81f5a649d7cff8e3b8ea354d6086af9a6727ef280f53e035749496ad26f0175f7cb1cf61106302556d7bc721f2cd4ee209ac33b664feda40f20998534af131608bdbbe0c62f7cbbe9d51775dabfbd42f602379ca586855507653db9b8290e8a1a698cab6d116ec1bd177ea248d0b72aff5a5603604711374b9d0df58208145dff3e21fc6f0f199d5a8af8375148c9382707cfe68d503d64f56c0bac0dc74658f79e28e648da10628698691d557269ada7245785ee298a4d290a4a76a69fb3dd12b21afa2a7cfa8ca5095eabc5200bde618e76bde22e23dc9e92e2b20dd135e86dfed5807e9d5afcbae7a2eb21f46a03e693efb833bc82d6ea9551ff68e6f02eb12bde7b193af5f59b90b7e37ed4b37dfa04d4f9dbdac5805aab40bf469f734198807b0bed269ac734db36eb602e6633bc7135eaa6c5f6be628c65e5759a9b3da62e0d09e4ab3b53750e0b9e75884a156bfc8dcbd3b6ff46790b353afa75583cef6003a7b3f2f7dbf34e9a61c4f797932790ba43f4cfc2a0d6a81444f81293a49b368d56fceeed68b6c7e2efce89070353db515a6d28f76e254e7de1ab7bd2c536ce287b1d58e65caebe55ae7c717bed0b6a51415d676bfd4f812b0de417992eaeefe15900e3f732c6395a7fdf9cfb723327557fdbaafb319b59f9be8307f31db5f00120dac9798032e3256b805a600bbcae94d65f4b080116ecd46935e4c6d78857b3e24c5ae0f4b93b1526d2ed9c202a6bca5e46e07941118247675b82528af8baf986f7b787a5ed730b63a67f3eb6927b767b7a34d6f1469edf2e3bde63abb2ff85e405295f120dba34c8d201d687072fcf8f1c90bcb15601e93f9290a42be19f768d51860a9b8e436e3e7f4e64cf73dd1457928445eb2d15771ac139bbc10dec07a2bd9644a4b49cc94ad45e75e7143d0959c6e5df1428c562e041e05d001d751b6b7d0b58e5dbd8afd0aa02b7b2b009df1cd31ae8e615cff78d40695dcc98f2b35b0277aa84e7d191f1c403301e21e9e2f966f3ec2d9eab71ecd293008e30a7f99e00f1803a6d7e20c0ef07002ef6192a0b9ea110077f7fa518711de02bed596cab76e1b4f988ba65dc07eea07b49f69a3ed33afedd2d6d85e854777b575431bd39e4d0b89abb6d1f2b766757bd0dbdef6b63df7a84b173aa8b2329dfa52b442c4c2b35d90bbbffcd28e0d3c9cc03b221479728211902ef7a51f85a7c7c74b6d7af1cab29599d16fd883680a9c7de2139fb8a1567dd32930fe2c386f1956fa6a9d06b3e99905f3ec6a83c64b6d3a5d267d95c1d349261fe69ab71607a5cddd82f5a7f935db771f153f18333859b4fd3cae36e3923fd13ef2e2a209671a2b9dc0a437e3d5747ebafbd30ba817dd4b1e4c7b9d4d01ab5ffdd55fbdf1655ff665be71cdfc95d35f1ee3f417c61a460cf8d0a0e92ebfc4ebe718ac6e8d9be60769c8b0d34c1cde445e53688f7c65d6545a392cf7d529fa4c83a62a88a9390f71729c9cc6d03bbd9f236f674e7cb1c0112895dbf0eb624680a8667d3af965ef2564e6c99c3034e222e6a79cfe227d383b8e390cd9e9c0bdd1ec10ef1d583e72672dc9034ddc23e69186affc42d5d7b79a11a0eb0f40676ed15d0bc4f334e9d5b74d7b55f3d069d47df0023165776244833c7bfea968be098bd67a00dbd966e200509780b7dcda1b00b04507a4f73ab4264db59eb536183419e3264fcb972d3043d819cdba6d405e2633fa756f68e33a48b7f8a98dd506ec69521deadf3a070a6ce2e5934f368db7c90083aeca6bf1969c0271af45f6a0a3458fddb6b9185ec58f20bd85bd0550b304e995eee92e330f0bbf0a207febb77e6bc640c13a83deca29304ebaede7e9a54203264c3047d0f296a0fbf0906bb017c48d20bd6827482ff726bbc0e2a6b8eb16565b91d4aa33294d9a4980fbb11f490df45086cfbb86644b90be1288a764c0f02d0f56a92348376475fbcb0b8c356ad5977bd59577e57e7e34f74d14db61d8a1db2f3a0f6ac3797e97a17db8f1ea22e99456d87ff21850d7ea33ddabafe6e07aabac5f803e82f4b14cc78b024b63b8c0bc80ea7505e93ef7a6673f3e7efc64f642db57356222c6d8096fb89d500d3b67824f977bd8dbca4cb8be6a4acaca5e8d7de37dce1d175dfc1a2573f7126f1db1075d803e827464ed6e3e1a7d0d41ba947a5881fa0480ba38f89c0e789c02738b49e7f909ac1b3f0a6681dd4db6693543fa09a4bb6d85a8d49144fd7611a0326e0dfc25bf65a9fdee456c5c505cd43ef3d59e74dd83265d6fc04ab5abda60c4d65c8d02b547fde0b3079944cd85d637fcda04d68d17948f976165d4a24f9af4a327728c93931083407e7dd474c52ff7a657be87c52ea05620ad40a2347020f539ddf377733ee3839f9b80c47aff809c7750bec8e9300dacacd3a5fd2a6903e9559e69a4ef98566deee8bf8eee755e8c3428c0cea0b70ed6c7642bee4c644c66f0725a245539a54d3743d5bb92f91a7b0a5416a82ac05e1f950aa91c6b1a5897502ef56fce16870b4f49526173abed5d6ffbd705ed07a7074780ac23deccbe6ad76dcaf2e23c47c6252f7e1d74e69bacf2e7ad16fe73177397c6390eac5dee69ab7c38dbbe74b5e9c8db2467755ad6b8d5a3e89a4cd7ec363ebbda74e9c28266c1bc94315699ba43df962c03c6611bcabe4a1101eaad4943d81d6c436251787cc442d0c561ecd15d61da6fe8c5fc9bfad992dada71970548539c4de739e228377de4d608cc47702e7d6a5c53f9535b5d1eef67f6bf9a9a74ebd23c9440bd3d5a0337001cb708d41b8b0e841f453005eb39b651c13c0386ab8cd1567835a6f72a30bd0ed20555d63be6dde4ee6ddb04d6a7231b7b1d531b37953386d93e41fa4b1d90af81f41560526db4bd63195bf7590a14af8cc91ef524691bd547adbac10c04d1ac3fbaa65d4f96e1d6007a3bdd8541f3d80f47676ff982db0332708efbd2cd56fc1a8a78289cb5dda16c1faab6a4488bd27a1c00d605ecf39b3701ecb5fbb576b0374dfb4810f810605f1ff614482ffad682a01608658f655c37f7c8039f5d1a8d5af502d901eb6a93f8c0d46b9d4e359141eb18e3cd5bf9371dcbb8a5ff928a9b5e8f8f5af5fd973dca55cd7a4ea170f1da17b0cb32a26557d30e707fb5ae1bbdacb22d57f7ec06d7ec46bf04d1e59f00b573ccab752d1f1297fddc8002e96ad30ba40b448dab05cf26ba1a7f1d4c3dbbf63bdef18e60a25ac400194e1e7ffcf193279129c1e7238fdc59799b5ff429d0feaadb476d31c07a208b022c42da8515b7f62633a6db149f3034e1cdae146ac6bb5b473cddc65dcf3bda35a609e203d0a193e0bce4ed2ec7282b6ffcdaf671d1f5d592bb87768ffa48e0025903d07181b2fbc84d74744f3d25281ef7f45df4eb5b596d5af64520bdeaae7acb7f915d698736a61dd493b6dd6b1bd7da67d559a4e8a08e8d8268dcd69c4f810d3c9a79a6fad1dbda0f1f8dfbd5ab14c0fade8b7c85d73ec4f3ee51266ff2470f265932ed136ae4dfc2ea6a6dbb4bf1aaea36edc36c0a286afb9ce39e75fd00bda9afa24cef7d75ed33471342e702e8fad6413a83ee6204ea63bdd71d308eb490761af950bf54aa1f7a4e7cd0cf7684957153746eb8a6c079d95f606ffa63fd23d2eb4eeb46a1f3ef23b832d51ffee11feebee94d6f8a82aded57e7db17f6160bda4f4ede34f260fc8ee0fc0a5e41cc9925f114b07cd3d5de7abd82c2af98a5c0b9c94780ae7f04e9eb80c9f8ad591e0d282dd4aebb575d37003cfdda5f2b55acd4b21fde3a9ce48af82f5dae5ee6876c1e9bcec86670a112bdb1bbc73493a9b48455ba1e37a69a927f890e01f97a1102f3314c80ae5f705ee182f47579abfe5b695ea9fdd003f50238a3dd8915b0ae3b60f8f9e7679c0cb332f96c22ea550070d5b529ffa6b04a3fda3ddd6a1b095c03ec678aabf619d135fd3acf80f4aacbc8adb93a058a6eda95ebed6f7fbb6bf30c7405d68da3c35f284f01e7bd90e3bed75d00b949934e7d0ffde26a048802b875b02ea9c083134d47d0dec978c62a806e04d83183aa341e41fa994cdb80951358463e08d625cf08d447f748ba02e686957bfb01e948a18bdd35c98fb639d6c1ba61c8f3d42ff46bd89d3001ac16f2fadd279544d6d0e5abfa372dac2bee629bbebb02984cad46b3723df112c0e9cbff80b0a75e33d054753da876c993edd7adbd09ac1bbee747a62f80d8db5a10fb0570c512b89be66131eb607cfdb91e7f7c296777ef6e06e8e679b5b4e863fd0f3d50af87dd04b0889b80b0e91a60c7f194becd6600bf2678d501f086769e6de3e6a64da1431ba7f6195960afea98326c1df74481a29f76651cc1ba612360af3428cc63bec05df717b83ff9e4123c16403711650798575d865d273382769f5b8dae762d8846c06ef845a600ba694690aebf1604a36df8d62c8f4bac1360a44969d68b3ee78174e30b9ceb76fb4c2d8ef49726bdf86cd8d66ca64001ab0255a61ac1ba7eb5ebda653601f78a7b506cb161d3ea3e093e44b79b0081f9f4fdd6f4284fbcf412005def970bdcb7207da2cc66c7269932e57980bd4ad97b494509bcd0c88f8078fc06c5bfe224acf3cef43d8149ef57e3f69ff69b3b6741a0db83aaddca98eed2a0ebde04d08bcec67f29e6da00758934821edd03e10202f0973d446d74be2600b8da37da43ed2b807d08dfe4dcd83e1356d99b326dc32ea740d16fb4c75ceb807d8cdbe41e35e8154fd95b90de35ea05a08b36eb80bdc22fb3370174f36c81e266ca155dca2e5a55eae24369d82b7cddaeb3d8b7207d9d3257f3d7445fb6b974576e01bbeeda0e53e1ebc0bdc22fb3a34c58d7282453055e56c26b1fff12a03c987caa0a70fed4d580d39465eb881c8de0b2482258d75ddb612a5cbbb6c68c610f937b04e3ebcfb5099c9ba668a87beca7fa5f2df386bd1a7bb51ee05eca110015c0325ff9716675d4ed8bc07aa5337bf277bbce6c0fc04ae42bb8d99e5eded44ec26a501ec1f72b6ee32b68d636cb4081e2d1101439283e75ad78e444d03ea61bdda3f6bcc2ab6ccb2a77c55d27bbb4ade33317602fc0577601c631ede8ae74635895a53dbac734d7dd2d5da4c179f429baf2f62eb2bece878ac79e4859658ee07f8adc3a3652a04080f6261050a7c1907951a0dd8200ee1bcbbb2cf02513e4867d63704f812678e3cd9da79e9ae6e29b37bfb8c8a137bd5945b34df47ae35b7effb4605da6ca5f5a62ed02edb65ae07e1190bd7f9eec95b7a4c0789550b4287fd92563fa4bcecaae34afa67dad34ea45b80242651baebbe22fb3493b01f2b18ccbf25d35beca2cbbf2e92ff74536e9ceb46fbdac8bf26fe3ae4681a269d9e6d27db5dc2d15e9b7bcba806005eacaaea4facb7d15bb40a269abacb2af92ffbaa6291a953dd2c1b0d17f9e7b04fb9bca392fdf367c49810201652f635635ec63f8757317d01cedeb468357f2bc2553655b86ee5752d6c39ce7f506e7232daf9546bd1e5c7024a01a6de3f4579a8becf5bce5bf28cfbdc4adb7632c7f3d6e53b9957eddde94761bf6ca29202f36d1d8f0ab96ba9ebffc57cdffb0a7db04f25e09d8ab3cebf6c34ebf2ff5f96a81b3890f965df117d5b34ef3f25f94671bb74a81020923081d8155c557ae871968ad3febf8cce7d1a7d26cedb314388f669be87c1de5aa2856fd6dddaef8adfd1a51405064d1658fee0abbcc7e8d9a36157b59fd97c54f056d1daf19052ee3c155e35fb3063e44050bf27c9c2fd57e8848f2ba3eca96eeaf2bb9cfadac0053d9262c77d9e7667e8023ead92eb31fe0477cc39a7e194d2bfe0d6be06b5c713ddf65f66bdc8c6df19751e04100540f421b2fa3f3c31e7f551e55ba879d1eafd5f3dd2b687cadda719dcbbd8c07d79936afe7b35f062e1e9678697adeb3bc9ef4be2e759d47ebeb16fe46f3fbff07b6480abb4dbe3a730000000049454e44ae426082010000ffffd16186b3c8b30400") - mewn.AddAsset("./templates","vuebasic/frontend/src/components/HelloWorld.vue","1f8b08000000000000ffbc92cf6adb4c14c5f7f31427faf82081ca7f5242a92c8b96ae03853cc158732d5d329a11335776d2a0772f23c931a49bae8a3773ef393ef33b924aa1aeb75aa8524069f884daea18f759ed9d687614b2a40065bbaddede3a8a5137348ee5badd2e82c6b7da72fdbccf1a92c7d990553f03c58847ba29d77aca5e1b3e55aa5c5f2f5465ac03f752297ae97d10183aeac10ade1460b4e8dbbbe908049221b86500168a0219b26935ee14307e524992d69b582cde2b5181e3e06a61efde5381930e88648fd8435a8ebb657d6667fc7975d0f53339b33ae8c8f5eddd4a5a72b7816242dc57ef219822560b14f6982d97b0f16e3e8d09518d3b55ae2fb5557993e7f86e0cb258fb9e4c062d12f83008413c2c772cf8f1f494860488da77bd77e404ded957e479a5ca28af96300754aadd4e609d0e0dbb5c7c5fe09eba84d0fbc8a97f8140560b9f286d3b76794bdcb452e021ccce331b690b6c379bff776a54ba68fd89c2947bf44ef2c8bfa8c076f565b61f7c3014f2da5b1f0a1cec3005a7a7d7043f38f35159c673cb42d7ff17f8dcbf207acbe6a394076d788889a87f99aa6863d83505bece733d8498227bcf4e28a49504ed2e7d1f369b2e4e4dfeb2c3f5fe3f4afcb7bd4fbf7fd4634c5f4b7abf95fa1d0000ffff2015f6aaaa030000") - mewn.AddAsset("./templates","vuebasic/frontend/src/main.js","1f8b08000000000000ff3c8ec1aa83301045f7f98a21ef2d924dba6fb0607fa1c57daaa3063419c6a42e8aff5e526d779773e070fd4c91133419a1e738837c6694561cb8263ab039d5446697a2c968da187a3f18e2d8e536f918ee9ea082de4d0b5af10d5cd97703fe1aabf3d3f2f8b0d2d9adb925c749290dd5055e0220e05a0ea9b2011843877c86b1e851d5445a006cdafccf3187a4e49f2392da8a4d5bf10e0000fffff79f8f3ccf000000") - mewn.AddAsset("./templates","vuebasic/frontend/src/wailsbridge.js","1f8b08000000000000ff6490b16ee33010447b7ec5c0956df8a4bbebceae2e758a0031e09a1257d2260cd72097528cc0ff1e9086dda460333bfb66b8edd600c0c9b24f788aec46c2badfe0efef3fff7e9d23250a8a67b2f81f74927031d57e9c38e11cc561a698580238410523296c941c1c263b73188b186989ac848be4885e1c55c420b11272af2ca1c169a2002765c72292279b085d66ef76d01237b0272cec3d3a424ee450391c92927590013a1186ecfdbd5463b6ad31f47996a87034d8ec155f06685b1c27c287e5802187dae026bfd8942855529ddeee22dd1bf55afe5206bdf5beb3fd3b78c0c83385c600af6aa3ee1fb4f5ddb4a98128de1f1ab07070b2344b496968a6a0a991b05e55611fc9bacb6af708dc1ceadad594773d98ef000000ffffff64ee69bc010000") - mewn.AddAsset("./templates","vuebasic/frontend/vue.config.js","1f8b08000000000000ffac924d8fd3301086effe15a38843824a56e246aad24359c469f78004071621d719a7ee3ab1e51997154dfe3b723eb65a54c1855c62cfe7f326af450645b4739d360d6ce03cac85301a721f9c42a212bb537977ffe1f6c7eddd17d86c20f3c1d551b1715d56c059c0cb760100804f1ca4e26abe026863b1932d56907d4befefa522ca5673561d62f7f8f16ac958310880612d06215a57478b253e791798e67dea204df715f75eaac70ad48cf27e5e9e045ad31a860dbcfbe3598f15534739cd9e91ca102de69969658394154b949138bf792873df357d63747ff44d913f6ccbd7c5f6d58d79ae8b847916837d639dac315c064cf7eb39963e773e7d584af8f7fb232a2e259169ba25b182f3a4a69a450d45f14f15da757c4dc44fa7f5db6d8f8e7b66dd3bd63d9dfe9f9e19395f4c002fc8e7e030e20fabc947d5c54c63643cc580cf7f779ae522fbf8577b1d17eb4c1e4b28adf92513d0a58dbc35bc4be6a30ab4b48417bba5be1a4f9f319c302c2db521b9b7f8c911ef0e987838449c561c1c710599754ada744efb0731acc5ef000000ffff9e11c4f462030000") - mewn.AddAsset("./templates","vuebasic/go.mod.template","1f8b08000000000000ffcacd4f29cd4955a8aed673cacc4b2caaf44bcc4dadad05040000ffffdcf9850916000000") - mewn.AddAsset("./templates","vuebasic/main.go.template","1f8b08000000000000ff8490c14ac4301086ef798adf089282a4ae8a4ac1c3ba97c5830757d873b6cdb6d134199294454adf5dda288217c921e11bbec9fc43aafe50ad46af8c63ccf4e4438260006f4dea8683ac7d5f5aad944b9d779f65af4f8eff299f94b15111e507670563c7c1d538a8686a5120a6605c8b910141a72138f0adb6d663ef836dce389bbe8579085160640c5044a81eb1b4949ba055d26b227191c19a68e3ddd1b47353606f9ad455c0eaeafaf672215b6dda2e55b8bf7bc8e0cd24ab2b808fa37c51bd9e269e0bcfbb6abe302793bb6554c165790cde25ed9ab23131958a48be475e6465b3cbcebf4a1d7f1d6ffd102af0f3d5cd7c96dfa72227954fc63562d9d70f791d9c28d8c4be020000ffff750bb905a1010000") - mewn.AddAsset("./templates","vuebasic/template.json","1f8b08000000000000ff7490b14eec301045fb7cc5c8f5db7d9b680b841062a929a8a09ed84362ad3d8eec311242fc3b72064b3474997beeb98afc390018c648e616cc4ba5e9ff2bcd1bda2b3c62f1d6fc6bbcac298ba362b3dfc4276edd0bccad00dd79467b3d8350dc020aa9f787d24b508be7a52dc004c80ee6ca2e90fbc9fb8f9c750cabac29b79d2742b8b0ac893fee02e111f5fb6189e8c3d1a678af86cd8442ae29d369bc398cd3e1342a7acb8985d839bf2ff653a1e722184203bc45e8e7cee6ea83eb2457060df49928bfd36fa6817ad9bb6587255b337c0ddf010000ffff9e6e33d879010000") -} diff --git a/cmd/fs.go b/cmd/fs.go deleted file mode 100644 index 88ba0d48c..000000000 --- a/cmd/fs.go +++ /dev/null @@ -1,156 +0,0 @@ -package cmd - -import ( - "crypto/md5" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" -) - -// FSHelper - Wrapper struct for File System utility commands -type FSHelper struct { -} - -// NewFSHelper - Returns a new FSHelper -func NewFSHelper() *FSHelper { - result := &FSHelper{} - return result -} - -// DirExists - Returns true if the given path resolves to a directory on the filesystem -func (fs *FSHelper) DirExists(path string) bool { - fi, err := os.Lstat(path) - if err != nil { - return false - } - - return fi.Mode().IsDir() -} - -// FileExists returns a boolean value indicating whether -// the given file exists -func (fs *FSHelper) FileExists(path string) bool { - fi, err := os.Lstat(path) - if err != nil { - return false - } - - return fi.Mode().IsRegular() -} - -// CreateFile creates a file at the given filename location with the contents -// set to the given data. It will create intermediary directories if needed. -func (fs *FSHelper) CreateFile(filename string, data []byte) error { - // Ensure directory exists - fs.MkDirs(filepath.Dir(filename)) - return ioutil.WriteFile(filename, data, 0644) -} - -// MkDirs creates the given nested directories. -// Returns error on failure -func (fs *FSHelper) MkDirs(fullPath string, mode ...os.FileMode) error { - var perms os.FileMode - perms = 0700 - if len(mode) == 1 { - perms = mode[0] - } - return os.MkdirAll(fullPath, perms) -} - -// CopyFile from source to target -func (fs *FSHelper) CopyFile(source, target string) error { - s, err := os.Open(source) - if err != nil { - return err - } - defer s.Close() - d, err := os.Create(target) - if err != nil { - return err - } - if _, err := io.Copy(d, s); err != nil { - d.Close() - return err - } - return d.Close() -} - -// Cwd returns the current working directory -// Aborts on Failure -func (fs *FSHelper) Cwd() string { - cwd, err := os.Getwd() - if err != nil { - log.Fatal("Unable to get working directory!") - } - return cwd -} - -// RemoveFile removes the given filename -func (fs *FSHelper) RemoveFile(filename string) error { - return os.Remove(filename) -} - -// RemoveFiles removes the given filenames -func (fs *FSHelper) RemoveFiles(files []string) error { - for _, filename := range files { - err := os.Remove(filename) - if err != nil { - return err - } - } - return nil -} - -// GetSubdirs will return a list of FQPs to subdirectories in the given directory -func (fs *FSHelper) GetSubdirs(dir string) (map[string]string, error) { - - // Read in the directory information - fileInfo, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - - // Allocate space for the list - subdirs := make(map[string]string) - - // Pull out the directories and store in the map as - // map["directoryName"] = "path/to/directoryName" - for _, file := range fileInfo { - if file.IsDir() { - subdirs[file.Name()] = filepath.Join(dir, file.Name()) - } - } - return subdirs, nil -} - -// MkDir creates the given directory. -// Returns error on failure -func (fs *FSHelper) MkDir(dir string) error { - return os.Mkdir(dir, 0700) -} - -// LoadAsString will attempt to load the given file and return -// its contents as a string -func (fs *FSHelper) LoadAsString(filename string) (string, error) { - bytes, err := ioutil.ReadFile(filename) - return string(bytes), err -} - -// FileMD5 returns the md5sum of the given file -func (fs *FSHelper) FileMD5(filename string) (string, error) { - f, err := os.Open(filename) - if err != nil { - return "", err - } - defer f.Close() - - h := md5.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - return fmt.Sprintf("%x", h.Sum(nil)), nil -} diff --git a/cmd/helpers.go b/cmd/helpers.go deleted file mode 100644 index d34c80eb2..000000000 --- a/cmd/helpers.go +++ /dev/null @@ -1,281 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "time" - - "github.com/leaanthony/slicer" - "github.com/leaanthony/spinner" -) - -var fs = NewFSHelper() - -// ValidateFrontendConfig checks if the frontend config is valid -func ValidateFrontendConfig(projectOptions *ProjectOptions) error { - if projectOptions.FrontEnd.Dir == "" { - return fmt.Errorf("Frontend directory not set in project.json") - } - if projectOptions.FrontEnd.Build == "" { - return fmt.Errorf("Frontend build command not set in project.json") - } - if projectOptions.FrontEnd.Install == "" { - return fmt.Errorf("Frontend install command not set in project.json") - } - if projectOptions.FrontEnd.Bridge == "" { - return fmt.Errorf("Frontend bridge config not set in project.json") - } - - return nil -} - -// InstallGoDependencies will run go get in the current directory -func InstallGoDependencies() error { - depSpinner := spinner.New("Installing Dependencies...") - depSpinner.SetSpinSpeed(50) - depSpinner.Start() - err := NewProgramHelper().RunCommand("go get") - if err != nil { - depSpinner.Error() - return err - } - depSpinner.Success() - return nil -} - -// BuildApplication will attempt to build the project based on the given inputs -func BuildApplication(binaryName string, forceRebuild bool, buildMode string, packageApp bool, projectOptions *ProjectOptions) error { - - // Generate Windows assets if needed - if runtime.GOOS == "windows" { - cleanUp := !packageApp - err := NewPackageHelper().PackageWindows(projectOptions, cleanUp) - if err != nil { - return err - } - } - - // Check Mewn is installed - err := CheckMewn() - if err != nil { - return err - } - - compileMessage := "Packing + Compiling project" - - if buildMode == BuildModeDebug { - compileMessage += " (Debug Mode)" - } - - packSpinner := spinner.New(compileMessage + "...") - packSpinner.SetSpinSpeed(50) - packSpinner.Start() - - buildCommand := slicer.String() - buildCommand.AddSlice([]string{"mewn", "build"}) - - if binaryName != "" { - buildCommand.Add("-o") - buildCommand.Add(binaryName) - } - - // If we are forcing a rebuild - if forceRebuild { - buildCommand.Add("-a") - } - - // Setup ld flags - ldflags := "-w -s " - if buildMode == BuildModeDebug { - ldflags = "" - } - - // Add windows flags - if runtime.GOOS == "windows" { - ldflags += "-H windowsgui " - } - - ldflags += "-X github.com/wailsapp/wails.BuildMode=" + buildMode - - buildCommand.AddSlice([]string{"-ldflags", ldflags}) - err = NewProgramHelper().RunCommandArray(buildCommand.AsSlice()) - if err != nil { - packSpinner.Error() - return err - } - packSpinner.Success() - - return nil -} - -// PackageApplication will attempt to package the application in a pltform dependent way -func PackageApplication(projectOptions *ProjectOptions) error { - // Package app - message := "Generating .app" - if runtime.GOOS == "windows" { - err := CheckWindres() - if err != nil { - return err - } - message = "Generating resource bundle" - } - packageSpinner := spinner.New(message) - packageSpinner.SetSpinSpeed(50) - packageSpinner.Start() - err := NewPackageHelper().Package(projectOptions) - if err != nil { - packageSpinner.Error() - return err - } - packageSpinner.Success() - return nil -} - -// BuildFrontend runs the given build command -func BuildFrontend(buildCommand string) error { - buildFESpinner := spinner.New("Building frontend...") - buildFESpinner.SetSpinSpeed(50) - buildFESpinner.Start() - err := NewProgramHelper().RunCommand(buildCommand) - if err != nil { - buildFESpinner.Error() - return err - } - buildFESpinner.Success() - return nil -} - -// CheckMewn checks if mewn is installed and if not, attempts to fetch it -func CheckMewn() (err error) { - programHelper := NewProgramHelper() - if !programHelper.IsInstalled("mewn") { - buildSpinner := spinner.New() - buildSpinner.SetSpinSpeed(50) - buildSpinner.Start("Installing Mewn asset packer...") - err := programHelper.InstallGoPackage("github.com/leaanthony/mewn/cmd/mewn") - if err != nil { - buildSpinner.Error() - return err - } - buildSpinner.Success() - } - return nil -} - -// CheckWindres checks if Windres is installed and if not, aborts -func CheckWindres() (err error) { - if runtime.GOOS != "windows" { - return nil - } - programHelper := NewProgramHelper() - if !programHelper.IsInstalled("windres") { - return fmt.Errorf("windres not installed. It comes by default with mingw. Ensure you have installed mingw correctly") - } - return nil -} - -// InstallFrontendDeps attempts to install the frontend dependencies based on the given options -func InstallFrontendDeps(projectDir string, projectOptions *ProjectOptions, forceRebuild bool, caller string) error { - - // Install frontend deps - err := os.Chdir(projectOptions.FrontEnd.Dir) - if err != nil { - return err - } - - // Check if frontend deps have been updated - feSpinner := spinner.New("Installing frontend dependencies (This may take a while)...") - feSpinner.SetSpinSpeed(50) - feSpinner.Start() - - requiresNPMInstall := true - - // Read in package.json MD5 - fs := NewFSHelper() - packageJSONMD5, err := fs.FileMD5("package.json") - if err != nil { - return err - } - - const md5sumFile = "package.json.md5" - - // If we aren't forcing the install and the md5sum file exists - if !forceRebuild && fs.FileExists(md5sumFile) { - // Yes - read contents - savedMD5sum, err := fs.LoadAsString(md5sumFile) - // File exists - if err == nil { - // Compare md5 - if savedMD5sum == packageJSONMD5 { - // Same - no need for reinstall - requiresNPMInstall = false - feSpinner.Success("Skipped frontend dependencies (-f to force rebuild)") - } - } - } - - // Md5 sum package.json - // Different? Build - if requiresNPMInstall || forceRebuild { - // Install dependencies - err = NewProgramHelper().RunCommand(projectOptions.FrontEnd.Install) - if err != nil { - feSpinner.Error() - return err - } - feSpinner.Success() - - // Update md5sum file - ioutil.WriteFile(md5sumFile, []byte(packageJSONMD5), 0644) - } - - bridgeFile := "wailsbridge.prod.js" - if caller == "serve" { - bridgeFile = "wailsbridge.js" - } - - // Copy bridge to project - _, filename, _, _ := runtime.Caller(1) - bridgeFileSource := filepath.Join(path.Dir(filename), "..", "..", "wailsruntimeassets", "bridge", bridgeFile) - bridgeFileTarget := filepath.Join(projectDir, projectOptions.FrontEnd.Dir, projectOptions.FrontEnd.Bridge, "wailsbridge.js") - err = fs.CopyFile(bridgeFileSource, bridgeFileTarget) - if err != nil { - return err - } - - // Build frontend - err = BuildFrontend(projectOptions.FrontEnd.Build) - if err != nil { - return err - } - return nil -} - -// ServeProject attempts to serve up the current project so that it may be connected to -// via the Wails bridge -func ServeProject(projectOptions *ProjectOptions, logger *Logger) error { - go func() { - time.Sleep(2 * time.Second) - logger.Green(">>>>> To connect, you will need to run '" + projectOptions.FrontEnd.Serve + "' in the '" + projectOptions.FrontEnd.Dir + "' directory <<<<<") - }() - location, err := filepath.Abs(projectOptions.BinaryName) - if err != nil { - return err - } - - logger.Yellow("Serving Application: " + location) - cmd := exec.Command(location) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { - return err - } - - return nil -} diff --git a/cmd/linux.go b/cmd/linux.go deleted file mode 100644 index 2f033f41a..000000000 --- a/cmd/linux.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" -) - -// LinuxDistribution is of type int -type LinuxDistribution int - -const ( - // Unknown is the catch-all distro - Unknown LinuxDistribution = 0 - // Ubuntu distribution - Ubuntu LinuxDistribution = 1 -) - -// DistroInfo contains all the information relating to a linux distribution -type DistroInfo struct { - Distribution LinuxDistribution - Description string - Release string - Codename string - DistributorID string -} - -// GetLinuxDistroInfo returns information about the running linux distribution -func GetLinuxDistroInfo() *DistroInfo { - result := &DistroInfo{Distribution: Unknown} - program := NewProgramHelper() - // Does lsb_release exist? - - lsbRelease := program.FindProgram("lsb_release") - if lsbRelease != nil { - stdout, _, _, err := lsbRelease.Run("-a") - if err != nil { - return result - } - - for _, line := range strings.Split(stdout, "\n") { - if strings.Contains(line, ":") { - // Iterate lines a - details := strings.Split(line, ":") - key := strings.TrimSpace(details[0]) - value := strings.TrimSpace(details[1]) - switch key { - case "Distributor ID": - result.DistributorID = value - switch value { - case "Ubuntu": - result.Distribution = Ubuntu - } - case "Description": - result.Description = value - case "Release": - result.Release = value - case "Codename": - result.Codename = value - - } - } - } - - } - return result -} - -// DpkgInstalled uses dpkg to see if a package is installed -func DpkgInstalled(packageName string) (bool, error) { - result := false - program := NewProgramHelper() - dpkg := program.FindProgram("dpkg") - if dpkg == nil { - return false, fmt.Errorf("cannot check dependencies: dpkg not found") - } - _, _, exitCode, _ := dpkg.Run("-L", packageName) - result = exitCode == 0 - return result, nil -} diff --git a/cmd/log.go b/cmd/log.go deleted file mode 100644 index 416383935..000000000 --- a/cmd/log.go +++ /dev/null @@ -1,130 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" - - "github.com/fatih/color" -) - -// Logger struct -type Logger struct { - errorOnly bool -} - -// NewLogger creates a new logger! -func NewLogger() *Logger { - return &Logger{errorOnly: false} -} - -// SetErrorOnly ensures that only errors are logged out -func (l *Logger) SetErrorOnly(errorOnly bool) { - l.errorOnly = errorOnly -} - -// Yellow - Outputs yellow text -func (l *Logger) Yellow(format string, a ...interface{}) { - if l.errorOnly { - return - } - color.New(color.FgHiYellow).PrintfFunc()(format+"\n", a...) -} - -// Yellowf - Outputs yellow text without the newline -func (l *Logger) Yellowf(format string, a ...interface{}) { - if l.errorOnly { - return - } - - color.New(color.FgHiYellow).PrintfFunc()(format, a...) -} - -// Green - Outputs Green text -func (l *Logger) Green(format string, a ...interface{}) { - if l.errorOnly { - return - } - - color.New(color.FgHiGreen).PrintfFunc()(format+"\n", a...) -} - -// White - Outputs White text -func (l *Logger) White(format string, a ...interface{}) { - if l.errorOnly { - return - } - - color.New(color.FgHiWhite).PrintfFunc()(format+"\n", a...) -} - -// WhiteUnderline - Outputs White text with underline -func (l *Logger) WhiteUnderline(format string, a ...interface{}) { - if l.errorOnly { - return - } - - l.White(format, a...) - l.White(l.underline(format)) -} - -// YellowUnderline - Outputs Yellow text with underline -func (l *Logger) YellowUnderline(format string, a ...interface{}) { - if l.errorOnly { - return - } - - l.Yellow(format, a...) - l.Yellow(l.underline(format)) -} - -// underline returns a string of a line, the length of the message given to it -func (l *Logger) underline(message string) string { - if l.errorOnly { - return "" - } - - return strings.Repeat("-", len(message)) -} - -// Red - Outputs Red text -func (l *Logger) Red(format string, a ...interface{}) { - if l.errorOnly { - return - } - - color.New(color.FgHiRed).PrintfFunc()(format+"\n", a...) -} - -// Error - Outputs an Error message -func (l *Logger) Error(format string, a ...interface{}) { - color.New(color.FgHiRed).PrintfFunc()("Error: "+format+"\n", a...) -} - -// PrintSmallBanner prints a condensed banner -func (l *Logger) PrintSmallBanner(message ...string) { - yellow := color.New(color.FgYellow).SprintFunc() - red := color.New(color.FgRed).SprintFunc() - msg := "" - if len(message) > 0 { - msg = " - " + message[0] - } - fmt.Printf("%s %s%s\n", yellow("Wails"), red(Version), msg) -} - -// PrintBanner prints the Wails banner before running commands -func (l *Logger) PrintBanner() error { - banner1 := ` _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ ` + "`" + `/ / / ___/ -| |/ |/ / /_/ / / (__ ) ` - banner2 := `|__/|__/\__,_/_/_/____/ ` - - l.Yellowf(banner1) - l.Red(Version) - l.Yellowf(banner2) - l.Green("https://wails.app") - l.White("The lightweight framework for web-like apps") - fmt.Println() - - return nil -} diff --git a/cmd/package.go b/cmd/package.go deleted file mode 100644 index 1144352e9..000000000 --- a/cmd/package.go +++ /dev/null @@ -1,263 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "image" - "io/ioutil" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "text/template" - "time" - - "github.com/jackmordaunt/icns" -) - -// PackageHelper helps with the 'wails package' command -type PackageHelper struct { - fs *FSHelper - log *Logger - system *SystemHelper -} - -// NewPackageHelper creates a new PackageHelper! -func NewPackageHelper() *PackageHelper { - return &PackageHelper{ - fs: NewFSHelper(), - log: NewLogger(), - system: NewSystemHelper(), - } -} - -type plistData struct { - Title string - Exe string - PackageID string - Version string - Author string - Date string -} - -func newPlistData(title, exe, packageID, version, author string) *plistData { - now := time.Now().Format(time.RFC822) - return &plistData{ - Title: title, - Exe: exe, - Version: version, - PackageID: packageID, - Author: author, - Date: now, - } -} - -func defaultString(val string, defaultVal string) string { - if val != "" { - return val - } - return defaultVal -} - -func (b *PackageHelper) getPackageFileBaseDir() string { - // Calculate template base dir - _, filename, _, _ := runtime.Caller(1) - return filepath.Join(path.Dir(filename), "packages", runtime.GOOS) -} - -// Package the application into a platform specific package -func (b *PackageHelper) Package(po *ProjectOptions) error { - switch runtime.GOOS { - case "darwin": - // Check we have the exe - if !b.fs.FileExists(po.BinaryName) { - return fmt.Errorf("cannot bundle non-existant binary file '%s'. Please build with 'wails build' first", po.BinaryName) - } - return b.packageOSX(po) - case "windows": - return b.PackageWindows(po, true) - case "linux": - return fmt.Errorf("linux is not supported at this time. Please see https://github.com/wailsapp/wails/issues/2") - default: - return fmt.Errorf("platform '%s' not supported for bundling yet", runtime.GOOS) - } -} - -// Package the application for OSX -func (b *PackageHelper) packageOSX(po *ProjectOptions) error { - - system := NewSystemHelper() - config, err := system.LoadConfig() - if err != nil { - return err - } - - name := defaultString(po.Name, "WailsTest") - exe := defaultString(po.BinaryName, name) - version := defaultString(po.Version, "0.1.0") - author := defaultString(config.Name, "Anonymous") - packageID := strings.Join([]string{"wails", name, version}, ".") - plistData := newPlistData(name, exe, packageID, version, author) - appname := po.Name + ".app" - - // Check binary exists - source := path.Join(b.fs.Cwd(), exe) - if !b.fs.FileExists(source) { - // We need to build! - return fmt.Errorf("Target '%s' not available. Has it been compiled yet?", exe) - } - - // Remove the existing package - os.RemoveAll(appname) - - exeDir := path.Join(b.fs.Cwd(), appname, "/Contents/MacOS") - b.fs.MkDirs(exeDir, 0755) - resourceDir := path.Join(b.fs.Cwd(), appname, "/Contents/Resources") - b.fs.MkDirs(resourceDir, 0755) - tmpl := template.New("infoPlist") - plistFile := filepath.Join(b.getPackageFileBaseDir(), "info.plist") - infoPlist, err := ioutil.ReadFile(plistFile) - if err != nil { - return err - } - tmpl.Parse(string(infoPlist)) - - // Write the template to a buffer - var tpl bytes.Buffer - err = tmpl.Execute(&tpl, plistData) - if err != nil { - return err - } - filename := path.Join(b.fs.Cwd(), appname, "Contents", "Info.plist") - err = ioutil.WriteFile(filename, tpl.Bytes(), 0644) - if err != nil { - return err - } - - // Copy executable - target := path.Join(exeDir, exe) - err = b.fs.CopyFile(source, target) - if err != nil { - return err - } - - err = os.Chmod(target, 0755) - if err != nil { - return err - } - err = b.packageIconOSX(resourceDir) - return err -} - -// PackageWindows packages the application for windows platforms -func (b *PackageHelper) PackageWindows(po *ProjectOptions, cleanUp bool) error { - basename := strings.TrimSuffix(po.BinaryName, ".exe") - - // Copy icon - tgtIconFile := filepath.Join(b.fs.Cwd(), basename+".ico") - if !b.fs.FileExists(tgtIconFile) { - srcIconfile := filepath.Join(b.getPackageFileBaseDir(), "wails.ico") - err := b.fs.CopyFile(srcIconfile, tgtIconFile) - if err != nil { - return err - } - } - - // Copy manifest - tgtManifestFile := filepath.Join(b.fs.Cwd(), basename+".exe.manifest") - if !b.fs.FileExists(tgtManifestFile) { - srcManifestfile := filepath.Join(b.getPackageFileBaseDir(), "wails.exe.manifest") - err := b.fs.CopyFile(srcManifestfile, tgtManifestFile) - if err != nil { - return err - } - } - - // Copy rc file - tgtRCFile := filepath.Join(b.fs.Cwd(), basename+".rc") - if !b.fs.FileExists(tgtRCFile) { - srcRCfile := filepath.Join(b.getPackageFileBaseDir(), "wails.rc") - rcfilebytes, err := ioutil.ReadFile(srcRCfile) - if err != nil { - return err - } - rcfiledata := strings.Replace(string(rcfilebytes), "$NAME$", basename, -1) - err = ioutil.WriteFile(tgtRCFile, []byte(rcfiledata), 0755) - if err != nil { - return err - } - } - - // Build syso - sysofile := filepath.Join(b.fs.Cwd(), basename+"-res.syso") - windresCommand := []string{"windres", "-o", sysofile, tgtRCFile} - err := NewProgramHelper().RunCommandArray(windresCommand) - if err != nil { - return err - } - - // clean up - if cleanUp { - filesToDelete := []string{tgtIconFile, tgtManifestFile, tgtRCFile} - err := b.fs.RemoveFiles(filesToDelete) - if err != nil { - return err - } - } - - return nil -} - -func (b *PackageHelper) copyIcon(resourceDir string) (string, error) { - - // TODO: Read this from project.json - const appIconFilename = "appicon.png" - srcIcon := path.Join(b.fs.Cwd(), appIconFilename) - - // Check if appicon.png exists - if !b.fs.FileExists(srcIcon) { - - // Install default icon - iconfile := filepath.Join(b.getPackageFileBaseDir(), "icon.png") - iconData, err := ioutil.ReadFile(iconfile) - if err != nil { - return "", err - } - err = ioutil.WriteFile(srcIcon, iconData, 0644) - if err != nil { - return "", err - } - } - return srcIcon, nil -} - -func (b *PackageHelper) packageIconOSX(resourceDir string) error { - - srcIcon, err := b.copyIcon(resourceDir) - if err != nil { - return err - } - tgtBundle := path.Join(resourceDir, "iconfile.icns") - imageFile, err := os.Open(srcIcon) - if err != nil { - return err - } - defer imageFile.Close() - srcImg, _, err := image.Decode(imageFile) - if err != nil { - return err - - } - dest, err := os.Create(tgtBundle) - if err != nil { - return err - - } - defer dest.Close() - if err := icns.Encode(dest, srcImg); err != nil { - return err - - } - return nil -} diff --git a/cmd/packages/darwin/icon.png b/cmd/packages/darwin/icon.png deleted file mode 100644 index 9f22be34b..000000000 Binary files a/cmd/packages/darwin/icon.png and /dev/null differ diff --git a/cmd/packages/darwin/info.plist b/cmd/packages/darwin/info.plist deleted file mode 100644 index eff0ddf0e..000000000 --- a/cmd/packages/darwin/info.plist +++ /dev/null @@ -1,11 +0,0 @@ - - - CFBundleName{{.Title}} - CFBundleExecutable{{.Exe}} - CFBundleIdentifier{{.PackageID}} - CFBundleVersion{{.Version}} - CFBundleGetInfoStringBuilt by {{.Author}} at {{.Date}} using Wails (https://wails.app) - CFBundleShortVersionString{{.Version}} - CFBundleIconFileiconfile - NSHighResolutionCapabletrue - \ No newline at end of file diff --git a/cmd/packages/windows/icon.png b/cmd/packages/windows/icon.png deleted file mode 100644 index 9f22be34b..000000000 Binary files a/cmd/packages/windows/icon.png and /dev/null differ diff --git a/cmd/packages/windows/wails.ico b/cmd/packages/windows/wails.ico deleted file mode 100644 index 9b62ac5b4..000000000 Binary files a/cmd/packages/windows/wails.ico and /dev/null differ diff --git a/cmd/packages/windows/wails.rc b/cmd/packages/windows/wails.rc deleted file mode 100644 index a0a6bc3f7..000000000 --- a/cmd/packages/windows/wails.rc +++ /dev/null @@ -1,2 +0,0 @@ -100 ICON "$NAME$.ico" -100 24 "$NAME$.exe.manifest" \ No newline at end of file diff --git a/cmd/prerequisites.go b/cmd/prerequisites.go deleted file mode 100644 index d23bfc0a5..000000000 --- a/cmd/prerequisites.go +++ /dev/null @@ -1,110 +0,0 @@ -package cmd - -import ( - "fmt" - "runtime" -) - -// Prerequisite defines a Prerequisite! -type Prerequisite struct { - Name string - Help string - Path string -} - -func newPrerequisite(name, help string) *Prerequisite { - return &Prerequisite{Name: name, Help: help} -} - -// Prerequisites is a list of things required to use Wails -type Prerequisites []*Prerequisite - -// Add given prereq object to list -func (p *Prerequisites) Add(prereq *Prerequisite) { - *p = append(*p, prereq) -} - -// GetRequiredPrograms returns a list of programs required for the platform -func GetRequiredPrograms() (*Prerequisites, error) { - switch runtime.GOOS { - case "darwin": - return getRequiredProgramsOSX(), nil - case "linux": - return getRequiredProgramsLinux(), nil - case "windows": - return getRequiredProgramsWindows(), nil - default: - return nil, fmt.Errorf("platform '%s' not supported at this time", runtime.GOOS) - } -} - -func getRequiredProgramsOSX() *Prerequisites { - result := &Prerequisites{} - result.Add(newPrerequisite("clang", "Please install with `xcode-select --install` and try again")) - result.Add(newPrerequisite("npm", "Please install from https://nodejs.org/en/download/ and try again")) - return result -} - -func getRequiredProgramsLinux() *Prerequisites { - result := &Prerequisites{} - distroInfo := GetLinuxDistroInfo() - switch distroInfo.Distribution { - case Ubuntu: - result.Add(newPrerequisite("gcc", "Please install with `sudo apt install build-essentials` and try again")) - result.Add(newPrerequisite("pkg-config", "Please install with `sudo apt install pkg-config` and try again")) - result.Add(newPrerequisite("npm", "Please install with `sudo apt install npm` and try again")) - - default: - result.Add(newPrerequisite("gcc", "Please install with your system package manager and try again")) - result.Add(newPrerequisite("pkg-config", "Please install with your system package manager and try again")) - result.Add(newPrerequisite("npm", "Please install from https://nodejs.org/en/download/ and try again")) - - } - return result -} - -// TODO: Test this on Windows -func getRequiredProgramsWindows() *Prerequisites { - result := &Prerequisites{} - result.Add(newPrerequisite("gcc", "Please install gcc from here and try again: http://tdm-gcc.tdragon.net/download. You will need to add the bin directory to your path, EG: C:\\TDM-GCC-64\\bin\\")) - result.Add(newPrerequisite("npm", "Please install node/npm from here and try again: https://nodejs.org/en/download/")) - return result -} - -// GetRequiredLibraries returns a list of libraries (packages) required for the platform -func GetRequiredLibraries() (*Prerequisites, error) { - switch runtime.GOOS { - case "darwin": - return getRequiredLibrariesOSX() - case "linux": - return getRequiredLibrariesLinux() - case "windows": - return getRequiredLibrariesWindows() - default: - return nil, fmt.Errorf("platform '%s' not supported at this time", runtime.GOOS) - } -} - -func getRequiredLibrariesOSX() (*Prerequisites, error) { - result := &Prerequisites{} - return result, nil -} - -func getRequiredLibrariesLinux() (*Prerequisites, error) { - result := &Prerequisites{} - distroInfo := GetLinuxDistroInfo() - switch distroInfo.Distribution { - case Ubuntu: - result.Add(newPrerequisite("libgtk-3-dev", "Please install with `sudo apt install libgtk-3-dev` and try again")) - result.Add(newPrerequisite("libwebkit2gtk-4.0-dev", "Please install with `sudo apt install libwebkit2gtk-4.0-dev` and try again")) - default: - result.Add(newPrerequisite("libgtk-3-dev", "Please install with your system package manager and try again")) - result.Add(newPrerequisite("libwebkit2gtk-4.0-dev", "Please install with your system package manager and try again")) - } - return result, nil -} - -func getRequiredLibrariesWindows() (*Prerequisites, error) { - result := &Prerequisites{} - return result, nil -} diff --git a/cmd/program.go b/cmd/program.go deleted file mode 100644 index 4cf59001b..000000000 --- a/cmd/program.go +++ /dev/null @@ -1,125 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "os/exec" - "path/filepath" - "strings" - "syscall" -) - -// ProgramHelper - Utility functions around installed applications -type ProgramHelper struct { - shell *ShellHelper -} - -// NewProgramHelper - Creates a new ProgramHelper -func NewProgramHelper() *ProgramHelper { - return &ProgramHelper{ - shell: NewShellHelper(), - } -} - -// IsInstalled tries to determine if the given binary name is installed -func (p *ProgramHelper) IsInstalled(programName string) bool { - _, err := exec.LookPath(programName) - return err == nil -} - -// Program - A struct to define an installed application/binary -type Program struct { - Name string `json:"name"` - Path string `json:"path"` -} - -// FindProgram attempts to find the given program on the system.FindProgram -// Returns a struct with the name and path to the program -func (p *ProgramHelper) FindProgram(programName string) *Program { - path, err := exec.LookPath(programName) - if err != nil { - return nil - } - path, err = filepath.Abs(path) - if err != nil { - return nil - } - return &Program{ - Name: programName, - Path: path, - } -} - -// GetFullPathToBinary returns the full path the the current binary -func (p *Program) GetFullPathToBinary() (string, error) { - return filepath.Abs(p.Path) -} - -// Run will execute the program with the given parameters -// Returns stdout + stderr as strings and an error if one occured -func (p *Program) Run(vars ...string) (stdout, stderr string, exitCode int, err error) { - command, err := p.GetFullPathToBinary() - if err != nil { - return "", "", 1, err - } - cmd := exec.Command(command, vars...) - var stdo, stde bytes.Buffer - cmd.Stdout = &stdo - cmd.Stderr = &stde - err = cmd.Run() - stdout = string(stdo.Bytes()) - stderr = string(stde.Bytes()) - - // https://stackoverflow.com/questions/10385551/get-exit-code-go - if err != nil { - // try to get the exit code - if exitError, ok := err.(*exec.ExitError); ok { - ws := exitError.Sys().(syscall.WaitStatus) - exitCode = ws.ExitStatus() - } else { - exitCode = 1 - if stderr == "" { - stderr = err.Error() - } - } - } else { - // success, exitCode should be 0 if go is ok - ws := cmd.ProcessState.Sys().(syscall.WaitStatus) - exitCode = ws.ExitStatus() - } - return -} - -// InstallGoPackage installs the given Go package -func (p *ProgramHelper) InstallGoPackage(packageName string) error { - args := strings.Split("get -u "+packageName, " ") - _, stderr, err := p.shell.Run("go", args...) - if err != nil { - fmt.Println(stderr) - } - return err -} - -// RunCommand runs the given command -func (p *ProgramHelper) RunCommand(command string) error { - args := strings.Split(command, " ") - return p.RunCommandArray(args) -} - -// RunCommandArray runs the command specified in the array -func (p *ProgramHelper) RunCommandArray(args []string) error { - program := args[0] - // TODO: Run FindProgram here and get the full path to the exe - program, err := exec.LookPath(program) - if err != nil { - fmt.Printf("ERROR: Looks like '%s' isn't installed. Please install and try again.", program) - return err - } - args = args[1:] - // fmt.Printf("RunCommandArray = %s %+v\n", program, args) - _, stderr, err := p.shell.Run(program, args...) - if err != nil { - fmt.Println(stderr) - } - return err -} diff --git a/cmd/project.go b/cmd/project.go deleted file mode 100644 index 478855c4c..000000000 --- a/cmd/project.go +++ /dev/null @@ -1,317 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/leaanthony/slicer" -) - -type author struct { - Name string `json:"name"` - Email string `json:"email"` -} - -type frontend struct { - Dir string `json:"dir"` - Install string `json:"install"` - Build string `json:"build"` - Bridge string `json:"bridge"` - Serve string `json:"serve"` -} - -type framework struct { - Name string `json:"name"` - BuildTag string `json:"buildtag"` - Options map[string]string `json:"options,omitempty"` -} - -// ProjectHelper is a helper struct for managing projects -type ProjectHelper struct { - log *Logger - system *SystemHelper - templates *TemplateHelper -} - -// NewProjectHelper creates a new Project helper struct -func NewProjectHelper() *ProjectHelper { - return &ProjectHelper{ - log: NewLogger(), - system: NewSystemHelper(), - templates: NewTemplateHelper(), - } -} - -// GenerateProject generates a new project using the options given -func (ph *ProjectHelper) GenerateProject(projectOptions *ProjectOptions) error { - - // exists := ph.templates.TemplateExists(projectOptions.Template) - - // if !exists { - // return fmt.Errorf("template '%s' is invalid", projectOptions.Template) - // } - - // Calculate project path - projectPath, err := filepath.Abs(projectOptions.OutputDirectory) - if err != nil { - return err - } - - _ = projectPath - - if fs.DirExists(projectPath) { - return fmt.Errorf("directory '%s' already exists", projectPath) - } - - // Create project directory - err = fs.MkDir(projectPath) - if err != nil { - return err - } - - // Create and save project config - err = projectOptions.WriteProjectConfig() - if err != nil { - return err - } - - err = ph.templates.InstallTemplate(projectPath, projectOptions) - if err != nil { - return err - } - - // // If we are on windows, dump a windows_resource.json - // if runtime.GOOS == "windows" { - // ph.GenerateWindowsResourceConfig(projectOptions) - // } - - ph.log.Yellow("Project '%s' generated in directory '%s'!", projectOptions.Name, projectOptions.OutputDirectory) - ph.log.Yellow("To compile the project, run 'wails build' in the project directory.") - return nil -} - -// // GenerateWindowsResourceConfig generates the default windows resource file -// func (ph *ProjectHelper) GenerateWindowsResourceConfig(po *ProjectOptions) { - -// fmt.Println(buffer.String()) - -// // vi.Build() -// // vi.Walk() -// // err := vi.WriteSyso(outPath, runtime.GOARCH) -// } - -// LoadProjectConfig loads the project config from the given directory -func (ph *ProjectHelper) LoadProjectConfig(dir string) (*ProjectOptions, error) { - po := ph.NewProjectOptions() - err := po.LoadConfig(dir) - return po, err -} - -// NewProjectOptions creates a new default set of project options -func (ph *ProjectHelper) NewProjectOptions() *ProjectOptions { - result := ProjectOptions{ - Name: "", - Description: "Enter your project description", - Version: "0.1.0", - BinaryName: "", - system: NewSystemHelper(), - log: NewLogger(), - templates: NewTemplateHelper(), - Author: &author{}, - } - - // Populate system config - config, err := ph.system.LoadConfig() - if err == nil { - result.Author.Name = config.Name - result.Author.Email = config.Email - } - - return &result -} - -// ProjectOptions holds all the options available for a project -type ProjectOptions struct { - Name string `json:"name"` - Description string `json:"description"` - Author *author `json:"author,omitempty"` - Version string `json:"version"` - OutputDirectory string `json:"-"` - UseDefaults bool `json:"-"` - Template string `json:"-"` - BinaryName string `json:"binaryname"` - FrontEnd *frontend `json:"frontend,omitempty"` - NPMProjectName string `json:"-"` - system *SystemHelper - log *Logger - templates *TemplateHelper - selectedTemplate *TemplateDetails -} - -// Defaults sets the default project template -func (po *ProjectOptions) Defaults() { - po.Template = "vuebasic" -} - -// PromptForInputs asks the user to input project details -func (po *ProjectOptions) PromptForInputs() error { - - processProjectName(po) - - processBinaryName(po) - - err := processOutputDirectory(po) - if err != nil { - return err - } - - // Process Templates - templateList := slicer.Interface() - options := slicer.String() - for _, templateDetails := range po.templates.TemplateList.details { - templateList.Add(templateDetails) - options.Add(fmt.Sprintf("%s - %s", templateDetails.Metadata.Name, templateDetails.Metadata.ShortDescription)) - } - - templateIndex := 0 - - if len(options.AsSlice()) > 1 { - templateIndex = PromptSelection("Please select a template", options.AsSlice(), 0) - } - - // After selection do this.... - po.selectedTemplate = templateList.AsSlice()[templateIndex].(*TemplateDetails) - - // Setup NPM Project name - po.NPMProjectName = strings.ToLower(strings.Replace(po.Name, " ", "_", -1)) - - // Fix template name - po.Template = strings.Split(po.selectedTemplate.Path, string(os.PathSeparator))[0] - - // // Populate template details - templateMetadata := po.selectedTemplate.Metadata - - err = processTemplateMetadata(templateMetadata, po) - if err != nil { - return err - } - - return nil -} - -// WriteProjectConfig writes the project configuration into -// the project directory -func (po *ProjectOptions) WriteProjectConfig() error { - targetDir, err := filepath.Abs(po.OutputDirectory) - if err != nil { - return err - } - - targetFile := filepath.Join(targetDir, "project.json") - filedata, err := json.MarshalIndent(po, "", " ") - if err != nil { - return err - } - - return ioutil.WriteFile(targetFile, filedata, 0600) -} - -// LoadConfig loads the project configuration file from the -// given directory -func (po *ProjectOptions) LoadConfig(projectDir string) error { - targetFile := filepath.Join(projectDir, "project.json") - rawBytes, err := ioutil.ReadFile(targetFile) - if err != nil { - return err - } - return json.Unmarshal(rawBytes, po) -} - -func computeBinaryName(projectName string) string { - if projectName == "" { - return "" - } - var binaryNameComputed = strings.ToLower(projectName) - binaryNameComputed = strings.Replace(binaryNameComputed, " ", "-", -1) - binaryNameComputed = strings.Replace(binaryNameComputed, string(filepath.Separator), "-", -1) - binaryNameComputed = strings.Replace(binaryNameComputed, ":", "-", -1) - return binaryNameComputed -} - -func processOutputDirectory(po *ProjectOptions) error { - // po.OutputDirectory - if po.OutputDirectory == "" { - po.OutputDirectory = PromptRequired("Project directory name", computeBinaryName(po.Name)) - } - projectPath, err := filepath.Abs(po.OutputDirectory) - if err != nil { - return err - } - - if NewFSHelper().DirExists(projectPath) { - return fmt.Errorf("directory '%s' already exists", projectPath) - } - - fmt.Println("Project Directory: " + po.OutputDirectory) - return nil -} - -func processProjectName(po *ProjectOptions) { - if po.Name == "" { - po.Name = Prompt("The name of the project", "My Project") - } - fmt.Println("Project Name: " + po.Name) - -} - -func processBinaryName(po *ProjectOptions) { - if po.BinaryName == "" { - var binaryNameComputed = computeBinaryName(po.Name) - po.BinaryName = Prompt("The output binary name", binaryNameComputed) - if runtime.GOOS == "windows" { - if !strings.HasSuffix(po.BinaryName, ".exe") { - po.BinaryName += ".exe" - } - } - } - fmt.Println("Output binary Name: " + po.BinaryName) -} - -func processTemplateMetadata(templateMetadata *TemplateMetadata, po *ProjectOptions) error { - if templateMetadata.FrontendDir != "" { - po.FrontEnd = &frontend{} - po.FrontEnd.Dir = templateMetadata.FrontendDir - } - if templateMetadata.Install != "" { - if po.FrontEnd == nil { - return fmt.Errorf("install set in template metadata but not frontenddir") - } - po.FrontEnd.Install = templateMetadata.Install - } - if templateMetadata.Build != "" { - if po.FrontEnd == nil { - return fmt.Errorf("build set in template metadata but not frontenddir") - } - po.FrontEnd.Build = templateMetadata.Build - } - - if templateMetadata.Bridge != "" { - if po.FrontEnd == nil { - return fmt.Errorf("bridge set in template metadata but not frontenddir") - } - po.FrontEnd.Bridge = templateMetadata.Bridge - } - - if templateMetadata.Serve != "" { - if po.FrontEnd == nil { - return fmt.Errorf("serve set in template metadata but not frontenddir") - } - po.FrontEnd.Serve = templateMetadata.Serve - } - return nil -} diff --git a/cmd/prompt.go b/cmd/prompt.go deleted file mode 100644 index 3a3adba46..000000000 --- a/cmd/prompt.go +++ /dev/null @@ -1,85 +0,0 @@ -package cmd - -import ( - "bufio" - "fmt" - "os" - "runtime" - "strconv" - "strings" -) - -// Prompt asks the user for a value -func Prompt(question string, defaultValue ...string) string { - var answer string - - if len(defaultValue) > 0 { - answer = defaultValue[0] - question = fmt.Sprintf("%s (%s)", question, answer) - } - fmt.Printf(question + ": ") - reader := bufio.NewReader(os.Stdin) - input, _ := reader.ReadString('\n') - EOL := "\n" - if runtime.GOOS == "windows" { - EOL = "\r\n" - } - input = strings.Replace(input, EOL, "", -1) - - if input != "" { - answer = input - } - - return answer -} - -// PromptRequired calls Prompt repeatedly until a value is given -func PromptRequired(question string, defaultValue ...string) string { - for { - result := Prompt(question, defaultValue...) - if result != "" { - return result - } - } -} - -// PromptSelection asks the user to choose an option -func PromptSelection(question string, options []string, optionalDefaultValue ...int) int { - - defaultValue := -1 - message := "Please choose an option" - fmt.Println(question + ":") - - if len(optionalDefaultValue) > 0 { - defaultValue = optionalDefaultValue[0] + 1 - message = fmt.Sprintf("%s [%d]", message, defaultValue) - } - - for index, option := range options { - fmt.Printf(" %d: %s\n", index+1, option) - } - - selectedValue := -1 - - for { - choice := Prompt(message) - if choice == "" && defaultValue > -1 { - selectedValue = defaultValue - 1 - break - } - - // index - number, err := strconv.Atoi(choice) - if err == nil { - if number > 0 && number <= len(options) { - selectedValue = number - 1 - break - } else { - continue - } - } - - } - - return selectedValue -} diff --git a/cmd/setup.go b/cmd/setup.go deleted file mode 100644 index 1d619dd05..000000000 --- a/cmd/setup.go +++ /dev/null @@ -1 +0,0 @@ -package cmd diff --git a/cmd/shell.go b/cmd/shell.go deleted file mode 100644 index b07ef451a..000000000 --- a/cmd/shell.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "bytes" - "os/exec" -) - -// ShellHelper helps with Shell commands -type ShellHelper struct { -} - -// NewShellHelper creates a new ShellHelper! -func NewShellHelper() *ShellHelper { - return &ShellHelper{} -} - -// Run the given command -func (sh *ShellHelper) Run(command string, vars ...string) (stdout, stderr string, err error) { - cmd := exec.Command(command, vars...) - var stdo, stde bytes.Buffer - cmd.Stdout = &stdo - cmd.Stderr = &stde - err = cmd.Run() - stdout = string(stdo.Bytes()) - stderr = string(stde.Bytes()) - return -} diff --git a/cmd/system.go b/cmd/system.go deleted file mode 100644 index c9baa48e5..000000000 --- a/cmd/system.go +++ /dev/null @@ -1,280 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "path/filepath" - "runtime" - "strconv" - "time" - - homedir "github.com/mitchellh/go-homedir" -) - -// SystemHelper - Defines everything related to the system -type SystemHelper struct { - log *Logger - fs *FSHelper - configFilename string - homeDir string - wailsSystemDir string - wailsSystemConfig string -} - -// NewSystemHelper - Creates a new System Helper -func NewSystemHelper() *SystemHelper { - result := &SystemHelper{ - fs: NewFSHelper(), - log: NewLogger(), - configFilename: "wails.json", - } - result.setSystemDirs() - return result -} - -// Internal -// setSystemDirs calculates the system directories it is interested in -func (s *SystemHelper) setSystemDirs() { - var err error - s.homeDir, err = homedir.Dir() - if err != nil { - log.Fatal("Cannot find home directory! Please file a bug report!") - } - // TODO: A better config system - s.wailsSystemDir = filepath.Join(s.homeDir, ".wails") - s.wailsSystemConfig = filepath.Join(s.wailsSystemDir, s.configFilename) -} - -// ConfigFileExists - Returns true if it does! -func (s *SystemHelper) ConfigFileExists() bool { - return s.fs.FileExists(s.wailsSystemConfig) -} - -// SystemDirExists - Returns true if it does! -func (s *SystemHelper) systemDirExists() bool { - return s.fs.DirExists(s.wailsSystemDir) -} - -// LoadConfig attempts to load the Wails system config -func (s *SystemHelper) LoadConfig() (*SystemConfig, error) { - return NewSystemConfig(s.wailsSystemConfig) -} - -// ConfigFileIsValid checks if the config file is valid -func (s *SystemHelper) ConfigFileIsValid() bool { - _, err := NewSystemConfig(s.wailsSystemConfig) - return err == nil -} - -// BackupConfig attempts to backup the system config file -func (s *SystemHelper) BackupConfig() (string, error) { - now := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) - backupFilename := s.wailsSystemConfig + "." + now - err := s.fs.CopyFile(s.wailsSystemConfig, backupFilename) - if err != nil { - return "", err - } - return backupFilename, nil -} - -func (s *SystemHelper) setup() error { - - // Try to load current values - ignore errors - config, err := s.LoadConfig() - defaultName := "" - defaultEmail := "" - if config != nil { - defaultName = config.Name - defaultEmail = config.Email - } - - systemConfig := make(map[string]string) - systemConfig["name"] = PromptRequired("What is your name", defaultName) - systemConfig["email"] = PromptRequired("What is your email address", defaultEmail) - - // Create the directory - err = s.fs.MkDirs(s.wailsSystemDir) - if err != nil { - return err - } - - // Save - configData, err := json.Marshal(&systemConfig) - if err != nil { - return err - } - err = ioutil.WriteFile(s.wailsSystemConfig, configData, 0755) - if err != nil { - return err - } - fmt.Println() - s.log.White("Wails config saved to: " + s.wailsSystemConfig) - s.log.White("Feel free to customise these settings.") - fmt.Println() - - return nil -} - -const introText = ` -Wails is a lightweight framework for creating web-like desktop apps in Go. -I'll need to ask you a few questions so I can fill in your project templates and then I will try and see if you have the correct dependencies installed. If you don't have the right tools installed, I'll try and suggest how to install them. -` - -// CheckInitialised checks if the system has been set up -// and if not, runs setup -func (s *SystemHelper) CheckInitialised() error { - if !s.systemDirExists() { - s.log.Yellow("System not initialised. Running setup.") - return s.setup() - } - return nil -} - -// Initialise attempts to set up the Wails system. -// An error is returns if there is a problem -func (s *SystemHelper) Initialise() error { - - // System dir doesn't exist - if !s.systemDirExists() { - s.log.Green("Welcome to Wails!") - s.log.Green(introText) - return s.setup() - } - - // Config doesn't exist - if !s.ConfigFileExists() { - s.log.Green("Looks like the system config is missing.") - s.log.Green("To get you back on track, I'll need to ask you a few things...") - return s.setup() - } - - // Config exists but isn't valid. - if !s.ConfigFileIsValid() { - s.log.Green("Looks like the system config got corrupted.") - backupFile, err := s.BackupConfig() - if err != nil { - s.log.Green("I tried to backup your config file but got this error: %s", err.Error()) - } else { - s.log.Green("Just in case you needed it, I backed up your config file here: %s", backupFile) - } - s.log.Green("To get you back on track, I'll need to ask you a few things...") - return s.setup() - } - - return s.setup() -} - -// SystemConfig - Defines system wode configuration data -type SystemConfig struct { - Name string `json:"name"` - Email string `json:"email"` -} - -// NewSystemConfig - Creates a new SystemConfig helper object -func NewSystemConfig(filename string) (*SystemConfig, error) { - result := &SystemConfig{} - err := result.load(filename) - return result, err -} - -// Save - Saves the system config to the given filename -func (sc *SystemConfig) Save(filename string) error { - // Convert config to JSON string - theJSON, err := json.MarshalIndent(sc, "", " ") - if err != nil { - return err - } - - // Write it out to the config file - return ioutil.WriteFile(filename, theJSON, 0644) -} - -func (sc *SystemConfig) load(filename string) error { - configData, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - // Load and unmarshall! - err = json.Unmarshal(configData, &sc) - if err != nil { - return err - } - return nil -} - -// CheckDependenciesSilent checks for dependencies but -// only outputs if there's an error -func CheckDependenciesSilent(logger *Logger) (bool, error) { - logger.SetErrorOnly(true) - result, err := CheckDependencies(logger) - logger.SetErrorOnly(false) - return result, err -} - -// CheckDependencies will look for Wails dependencies on the system -// Errors are reported in error and the bool return value is whether -// the dependencies are all installed. -func CheckDependencies(logger *Logger) (bool, error) { - - switch runtime.GOOS { - case "darwin": - logger.Yellow("Detected Platform: OSX") - case "windows": - logger.Yellow("Detected Platform: Windows") - case "linux": - logger.Yellow("Detected Platform: Linux") - default: - return false, fmt.Errorf("Platform %s is currently not supported", runtime.GOOS) - } - - logger.Yellow("Checking for prerequisites...") - // Check we have a cgo capable environment - - requiredPrograms, err := GetRequiredPrograms() - if err != nil { - return false, nil - } - errors := false - programHelper := NewProgramHelper() - for _, program := range *requiredPrograms { - bin := programHelper.FindProgram(program.Name) - if bin == nil { - errors = true - logger.Red("Program '%s' not found. %s", program.Name, program.Help) - } else { - logger.Green("Program '%s' found: %s", program.Name, bin.Path) - } - } - - // Linux has library deps - if runtime.GOOS == "linux" { - // Check library prerequisites - requiredLibraries, err := GetRequiredLibraries() - if err != nil { - return false, err - } - distroInfo := GetLinuxDistroInfo() - for _, library := range *requiredLibraries { - switch distroInfo.Distribution { - case Ubuntu: - installed, err := DpkgInstalled(library.Name) - if err != nil { - return false, err - } - if !installed { - errors = true - logger.Red("Library '%s' not found. %s", library.Name, library.Help) - } else { - logger.Green("Library '%s' installed.", library.Name) - } - default: - return false, fmt.Errorf("unable to check libraries on distribution '%s'. Please ensure that the '%s' equivalent is installed", distroInfo.DistributorID, library.Name) - } - } - } - logger.White("") - - return !errors, err -} diff --git a/cmd/templates.go b/cmd/templates.go deleted file mode 100644 index 55e8c63df..000000000 --- a/cmd/templates.go +++ /dev/null @@ -1,154 +0,0 @@ -package cmd - -import ( - "bytes" - "encoding/json" - "log" - "path/filepath" - "regexp" - "strings" - "text/template" - - mewn "github.com/leaanthony/mewn" - mewnlib "github.com/leaanthony/mewn/lib" - "github.com/leaanthony/slicer" -) - -// TemplateMetadata holds all the metadata for a Wails template -type TemplateMetadata struct { - Name string `json:"name"` - ShortDescription string `json:"shortdescription"` - Description string `json:"description"` - Install string `json:"install"` - Build string `json:"build"` - Author string `json:"author"` - Created string `json:"created"` - FrontendDir string `json:"frontenddir"` - Serve string `json:"serve"` - Bridge string `json:"bridge"` -} - -// TemplateDetails holds information about a specific template -type TemplateDetails struct { - BasePath string - Path string - Metadata *TemplateMetadata -} - -// TemplateList is a list of available templates -type TemplateList struct { - details map[string]*TemplateDetails -} - -// NewTemplateList creates a new TemplateList object -func NewTemplateList(filenames *mewnlib.FileGroup) *TemplateList { - // Iterate each template and store information - - result := &TemplateList{details: make(map[string]*TemplateDetails)} - - entries := slicer.String() - entries.AddSlice(filenames.Entries()) - - // Find all template.json files - metadataFiles := entries.Filter(func(filename string) bool { - match, _ := regexp.MatchString("(.)+template.json$", filename) - return match - }) - - // Load each metadata file - metadataFiles.Each(func(filename string) { - fileData := filenames.Bytes(filename) - var metadata TemplateMetadata - err := json.Unmarshal(fileData, &metadata) - if err != nil { - log.Fatalf("corrupt metadata for template: %s", filename) - } - path := strings.Split(filename, string(filepath.Separator))[0] - thisTemplate := &TemplateDetails{Path: path, Metadata: &metadata} - result.details[filename] = thisTemplate - }) - - return result -} - -// Template holds details about a Wails template -type Template struct { - Name string - Path string - Description string -} - -// TemplateHelper is a utility object to help with processing templates -type TemplateHelper struct { - TemplateList *TemplateList - Files *mewnlib.FileGroup -} - -// NewTemplateHelper creates a new template helper -func NewTemplateHelper() *TemplateHelper { - files := mewn.Group("./templates") - - return &TemplateHelper{ - TemplateList: NewTemplateList(files), - Files: files, - } -} - -// InstallTemplate installs the template given in the project options to the -// project path given -func (t *TemplateHelper) InstallTemplate(projectPath string, projectOptions *ProjectOptions) error { - - // Get template files - templatePath := projectOptions.selectedTemplate.Path - - templateFilenames := slicer.String() - templateFilenames.AddSlice(projectOptions.templates.Files.Entries()) - - templateJSONFilename := filepath.Join(templatePath, "template.json") - - templateFiles := templateFilenames.Filter(func(filename string) bool { - return strings.HasPrefix(filename, templatePath) && filename != templateJSONFilename - }) - - var err error - templateFiles.Each(func(templateFile string) { - - // Setup filenames - relativeFilename := strings.TrimPrefix(templateFile, templatePath)[1:] - targetFilename, err := filepath.Abs(filepath.Join(projectOptions.OutputDirectory, relativeFilename)) - if err != nil { - return - } - filedata := projectOptions.templates.Files.Bytes(templateFile) - - // If file is a template, process it - if strings.HasSuffix(templateFile, ".template") { - templateData := projectOptions.templates.Files.String(templateFile) - tmpl := template.New(templateFile) - tmpl.Parse(templateData) - var tpl bytes.Buffer - err = tmpl.Execute(&tpl, projectOptions) - if err != nil { - return - } - - // Remove template suffix - targetFilename = strings.TrimSuffix(targetFilename, ".template") - - // Set the filedata to the template result - filedata = tpl.Bytes() - } - - // Normal file, just copy it - err = fs.CreateFile(targetFilename, filedata) - if err != nil { - return - } - }) - - if err != nil { - return err - } - - return nil -} diff --git a/cmd/templates/vuebasic/frontend/README.md b/cmd/templates/vuebasic/frontend/README.md deleted file mode 100644 index 6953f66d0..000000000 --- a/cmd/templates/vuebasic/frontend/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# vue basic - -## Project setup - -``` -npm install -``` - -### Compiles and hot-reloads for development - -``` -npm run serve -``` - -### Compiles and minifies for production - -``` -npm run build -``` - -### Run your tests - -``` -npm run test -``` - -### Lints and fixes files - -``` -npm run lint -``` - -### Customize configuration - -See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/cmd/templates/vuebasic/frontend/babel.config.js b/cmd/templates/vuebasic/frontend/babel.config.js deleted file mode 100644 index ba179669a..000000000 --- a/cmd/templates/vuebasic/frontend/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@vue/app' - ] -} diff --git a/cmd/templates/vuebasic/frontend/package-lock.json b/cmd/templates/vuebasic/frontend/package-lock.json deleted file mode 100644 index 005d6f74b..000000000 --- a/cmd/templates/vuebasic/frontend/package-lock.json +++ /dev/null @@ -1,11109 +0,0 @@ -{ - "name": "my-vue-app-01", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.2.2.tgz", - "integrity": "sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.2", - "@babel/helpers": "^7.2.0", - "@babel/parser": "^7.2.2", - "@babel/template": "^7.2.2", - "@babel/traverse": "^7.2.2", - "@babel/types": "^7.2.2", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.10", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz", - "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==", - "dev": true, - "requires": { - "@babel/types": "^7.3.2", - "jsesc": "^2.5.1", - "lodash": "^4.17.10", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-call-delegate": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz", - "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.0.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz", - "integrity": "sha512-tdW8+V8ceh2US4GsYdNVNoohq5uVwOf9k6krjwW4E1lINcHgttnWcNqgdoessn12dAy8QkbezlbQh2nXISNY+A==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-member-expression-to-functions": "^7.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.2.3" - } - }, - "@babel/helper-define-map": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz", - "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/types": "^7.0.0", - "lodash": "^4.17.10" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz", - "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", - "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-transforms": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz", - "integrity": "sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/template": "^7.2.2", - "@babel/types": "^7.2.2", - "lodash": "^4.17.10" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz", - "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==", - "dev": true, - "requires": { - "lodash": "^4.17.10" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz", - "integrity": "sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.2.3", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", - "dev": true, - "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", - "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-wrap-function": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", - "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.2.0" - } - }, - "@babel/helpers": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz", - "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==", - "dev": true, - "requires": { - "@babel/template": "^7.1.2", - "@babel/traverse": "^7.1.5", - "@babel/types": "^7.3.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz", - "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", - "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0", - "@babel/plugin-syntax-async-generators": "^7.2.0" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz", - "integrity": "sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.3.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-proposal-decorators": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.3.0.tgz", - "integrity": "sha512-3W/oCUmsO43FmZIqermmq6TKaRSYhmh/vybPfVFwQWdSb8xwki38uAIvknCRzuyHRuYfCYmJzL9or1v0AffPjg==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.3.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-decorators": "^7.2.0" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", - "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^7.2.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz", - "integrity": "sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz", - "integrity": "sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.2.0" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-decorators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz", - "integrity": "sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", - "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", - "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz", - "integrity": "sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz", - "integrity": "sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.10" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz", - "integrity": "sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-define-map": "^7.1.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz", - "integrity": "sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz", - "integrity": "sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz", - "integrity": "sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz", - "integrity": "sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz", - "integrity": "sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz", - "integrity": "sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz", - "integrity": "sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz", - "integrity": "sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", - "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz", - "integrity": "sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==", - "dev": true, - "requires": { - "regexp-tree": "^0.1.0" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", - "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", - "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz", - "integrity": "sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA==", - "dev": true, - "requires": { - "@babel/helper-call-delegate": "^7.1.0", - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz", - "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==", - "dev": true, - "requires": { - "regenerator-transform": "^0.13.3" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.2.0.tgz", - "integrity": "sha512-jIgkljDdq4RYDnJyQsiWbdvGeei/0MOTtSHKO/rfbd/mXBxNpdlulMx49L0HQ4pug1fXannxoqCI+fYSle9eSw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "resolve": "^1.8.1", - "semver": "^5.5.1" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", - "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz", - "integrity": "sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz", - "integrity": "sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" - } - }, - "@babel/preset-env": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.3.1.tgz", - "integrity": "sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.3.1", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.2.0", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.2.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.2.0", - "@babel/plugin-transform-classes": "^7.2.0", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.2.0", - "@babel/plugin-transform-dotall-regex": "^7.2.0", - "@babel/plugin-transform-duplicate-keys": "^7.2.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.2.0", - "@babel/plugin-transform-function-name": "^7.2.0", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.2.0", - "@babel/plugin-transform-modules-commonjs": "^7.2.0", - "@babel/plugin-transform-modules-systemjs": "^7.2.0", - "@babel/plugin-transform-modules-umd": "^7.2.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.3.0", - "@babel/plugin-transform-new-target": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.2.0", - "@babel/plugin-transform-parameters": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.2.0", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.2.0", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.2.0", - "browserslist": "^4.3.4", - "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", - "semver": "^5.3.0" - } - }, - "@babel/runtime": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", - "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.12.0" - } - }, - "@babel/runtime-corejs2": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.3.1.tgz", - "integrity": "sha512-YpO13776h3e6Wy8dl2J8T9Qwlvopr+b4trCEhHE+yek6yIqV8sx6g3KozdHMbXeBpjosbPi+Ii5Z7X9oXFHUKA==", - "dev": true, - "requires": { - "core-js": "^2.5.7", - "regenerator-runtime": "^0.12.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.4.tgz", - "integrity": "sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==", - "dev": true - } - } - }, - "@babel/template": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", - "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.2.2", - "@babel/types": "^7.2.2" - } - }, - "@babel/traverse": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", - "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.2.3", - "@babel/types": "^7.2.2", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.10" - } - }, - "@babel/types": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz", - "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^2.0.0" - } - }, - "@intervolga/optimize-cssnano-plugin": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz", - "integrity": "sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA==", - "dev": true, - "requires": { - "cssnano": "^4.0.0", - "cssnano-preset-default": "^4.0.0", - "postcss": "^7.0.0" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@soda/friendly-errors-webpack-plugin": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz", - "integrity": "sha512-cWKrGaFX+rfbMrAxVv56DzhPNqOJPZuNIS2HGMELtgGzb+vsMzyig9mml5gZ/hr2BGtSLV+dP2LUEuAL8aG2mQ==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "error-stack-parser": "^2.0.0", - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "@types/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz", - "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==", - "dev": true - }, - "@vue/babel-helper-vue-jsx-merge-props": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0-beta.2.tgz", - "integrity": "sha512-Yj92Q1GcGjjctecBfnBmVqKSlMdyZaVq10hlZB4HSd1DJgu4cWgpEImJSzcJRUCZmas6UigwE7f4IjJuQs+JvQ==", - "dev": true - }, - "@vue/babel-plugin-transform-vue-jsx": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.0.0-beta.2.tgz", - "integrity": "sha512-fvAymRZAPHitomRE+jIipWRj0STXNSMqeOSdOFu9Ffjqg9WGOxSdCjORxexManfZ2y5QDv7gzI1xfgprsK3nlw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.2", - "html-tags": "^2.0.0", - "lodash.kebabcase": "^4.1.1", - "svg-tags": "^1.0.0" - } - }, - "@vue/babel-preset-app": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-3.4.0.tgz", - "integrity": "sha512-P7IaOFtMUd5iic2PH/iY6YPgtPnyd7SzA+ACv1283F5RcLutTURhl2smC1cWUJFGVrUhTmsYEcbS4+06wKymWw==", - "dev": true, - "requires": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-decorators": "^7.1.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/preset-env": "^7.0.0", - "@babel/runtime": "^7.0.0", - "@babel/runtime-corejs2": "^7.2.0", - "@vue/babel-preset-jsx": "^1.0.0-beta.2", - "babel-plugin-dynamic-import-node": "^2.2.0", - "core-js": "^2.6.3" - }, - "dependencies": { - "core-js": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.4.tgz", - "integrity": "sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==", - "dev": true - } - } - }, - "@vue/babel-preset-jsx": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.0.0-beta.2.tgz", - "integrity": "sha512-nZoAKBR/h6iPMQ66ieQcIdlpPBmqhtUUcgjBS541jIVxSog1rwzrc00jlsuecLonzUMWPU0PabyitsG74vhN1w==", - "dev": true, - "requires": { - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.2", - "@vue/babel-plugin-transform-vue-jsx": "^1.0.0-beta.2", - "@vue/babel-sugar-functional-vue": "^1.0.0-beta.2", - "@vue/babel-sugar-inject-h": "^1.0.0-beta.2", - "@vue/babel-sugar-v-model": "^1.0.0-beta.2", - "@vue/babel-sugar-v-on": "^1.0.0-beta.2" - } - }, - "@vue/babel-sugar-functional-vue": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.0.0-beta.2.tgz", - "integrity": "sha512-5qvi4hmExgjtrESDk0vflL69dIxkDAukJcYH9o4663E8Nh12Jpbmr+Ja8WmgkAPtTVhk90UVcVUFCCZLHBmhkQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-inject-h": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.0.0-beta.2.tgz", - "integrity": "sha512-qGXZ6yE+1trk82xCVJ9j3shsgI+R2ePj3+o8b2Ee7JNaRqQvMfTwpgx5BRlk4q1+CTjvYexdqBS+q4Kg7sSxcg==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-v-model": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.0.0-beta.2.tgz", - "integrity": "sha512-63US3IMEtATJzzK2le/Na53Sk2bp3LHfwZ8eMFwbTaz6e2qeV9frBl3ZYaha64ghT4IDSbrDXUmm0J09EAzFfA==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.2", - "@vue/babel-plugin-transform-vue-jsx": "^1.0.0-beta.2", - "camelcase": "^5.0.0", - "html-tags": "^2.0.0", - "svg-tags": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - } - } - }, - "@vue/babel-sugar-v-on": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.0.0-beta.2.tgz", - "integrity": "sha512-XH/m3k11EKdMY0MrTg4+hQv8BFM8juzHT95chYkgxDmvDdVJnSCuf9+mcysEJttWD4PVuUGN7EHoIWsIhC0dRw==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.0.0-beta.2", - "camelcase": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - } - } - }, - "@vue/cli-overlay": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-3.4.0.tgz", - "integrity": "sha512-uLfQZvMChAf3UQNR+WN8a7vAPqvaw2tJs1TrNxPg+Dr7bm7HWoitvFremF0vLWkxIRM5e+VJgYV3wHk9EwWhzg==", - "dev": true - }, - "@vue/cli-plugin-babel": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/cli-plugin-babel/-/cli-plugin-babel-3.4.0.tgz", - "integrity": "sha512-8ViOzJa8UqUnmMl1422t8EIlCUc5PegSMsdU6xoqfavL83uEGjR+fE4gAI+g7xKo7Qk9+8Z8VvaredXMbmxCzA==", - "dev": true, - "requires": { - "@babel/core": "^7.0.0", - "@vue/babel-preset-app": "^3.4.0", - "@vue/cli-shared-utils": "^3.4.0", - "babel-loader": "^8.0.5", - "webpack": ">=4 < 4.29" - } - }, - "@vue/cli-plugin-eslint": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-3.4.0.tgz", - "integrity": "sha512-KbUpN3Zd/V5zCah9nT9cukTHmd9g4IRskyuIeBw5KZqRDoUgCS7I2+OWlcAMneRuqZwgFbTFYmr9N3s6gz4SVg==", - "dev": true, - "requires": { - "@vue/cli-shared-utils": "^3.4.0", - "babel-eslint": "^10.0.1", - "eslint": "^4.19.1", - "eslint-loader": "^2.1.1", - "eslint-plugin-vue": "^4.7.1", - "globby": "^9.0.0", - "webpack": ">=4 < 4.29" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", - "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", - "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", - "progress": "^2.0.0", - "regexpp": "^1.0.1", - "require-uncached": "^1.0.3", - "semver": "^5.3.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" - } - }, - "eslint-plugin-vue": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-4.7.1.tgz", - "integrity": "sha512-esETKhVMI7Vdli70Wt4bvAwnZBJeM0pxVX9Yb0wWKxdCJc2EADalVYK/q2FzMw8oKN0wPMdqVCKS8kmR89recA==", - "dev": true, - "requires": { - "vue-eslint-parser": "^2.0.3" - } - }, - "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "@vue/cli-service": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-3.4.0.tgz", - "integrity": "sha512-AtLiin5Jlw0ULKXJtBhUaykz0VzDgYq2RCf7nxfB7Vsi5fTbJyOVeWYe9KsnsM6VTRBWRUI8NzPPMYxV2uxtQA==", - "dev": true, - "requires": { - "@intervolga/optimize-cssnano-plugin": "^1.0.5", - "@soda/friendly-errors-webpack-plugin": "^1.7.1", - "@vue/cli-overlay": "^3.4.0", - "@vue/cli-shared-utils": "^3.4.0", - "@vue/component-compiler-utils": "^2.5.2", - "@vue/preload-webpack-plugin": "^1.1.0", - "@vue/web-component-wrapper": "^1.2.0", - "acorn": "^6.0.6", - "acorn-walk": "^6.1.1", - "address": "^1.0.3", - "autoprefixer": "^9.4.7", - "cache-loader": "^2.0.1", - "case-sensitive-paths-webpack-plugin": "^2.2.0", - "chalk": "^2.4.2", - "clipboardy": "^1.2.3", - "cliui": "^4.1.0", - "copy-webpack-plugin": "^4.6.0", - "css-loader": "^1.0.1", - "cssnano": "^4.1.8", - "debug": "^4.1.1", - "escape-string-regexp": "^1.0.5", - "file-loader": "^3.0.1", - "fs-extra": "^7.0.1", - "globby": "^9.0.0", - "hash-sum": "^1.0.2", - "html-webpack-plugin": "^3.2.0", - "launch-editor-middleware": "^2.2.1", - "lodash.defaultsdeep": "^4.6.0", - "lodash.mapvalues": "^4.6.0", - "lodash.transform": "^4.6.0", - "mini-css-extract-plugin": "^0.5.0", - "minimist": "^1.2.0", - "ora": "^3.0.0", - "portfinder": "^1.0.20", - "postcss-loader": "^3.0.0", - "read-pkg": "^4.0.1", - "semver": "^5.6.0", - "slash": "^2.0.0", - "source-map-url": "^0.4.0", - "ssri": "^6.0.1", - "string.prototype.padend": "^3.0.0", - "terser-webpack-plugin": "^1.2.1", - "thread-loader": "^2.1.2", - "url-loader": "^1.1.2", - "vue-loader": "^15.6.2", - "webpack": ">=4 < 4.29", - "webpack-bundle-analyzer": "^3.0.3", - "webpack-chain": "^4.11.0", - "webpack-dev-server": "^3.1.14", - "webpack-merge": "^4.2.1", - "yorkie": "^2.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", - "dev": true - } - } - }, - "@vue/cli-shared-utils": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-3.4.0.tgz", - "integrity": "sha512-w9j2qIroUUC2ym4Lb0lLMdlGmYThhwV0OizOEVigB5eZOEUEBV2Mv43K+nWJ6OyRBACnvhJTDi1gIwJo8zUvOw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "execa": "^1.0.0", - "joi": "^14.3.0", - "launch-editor": "^2.2.1", - "lru-cache": "^5.1.1", - "node-ipc": "^9.1.1", - "opn": "^5.3.0", - "ora": "^3.0.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "semver": "^5.5.0", - "string.prototype.padstart": "^3.0.0" - } - }, - "@vue/component-compiler-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.5.2.tgz", - "integrity": "sha512-3exq9O89GXo9E+CGKzgURCbasG15FtFMs8QRrCUVWGaKue4Egpw41MHb3Avtikv1VykKfBq3FvAnf9Nx3sdVJg==", - "dev": true, - "requires": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.14", - "postcss-selector-parser": "^5.0.0", - "prettier": "1.16.3", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.8.2" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "@vue/preload-webpack-plugin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.0.tgz", - "integrity": "sha512-rcn2KhSHESBFMPj5vc5X2pI9bcBNQQixvJXhD5gZ4rN2iym/uH2qfDSQfUS5+qwiz0a85TCkeUs6w6jxFDudbw==", - "dev": true - }, - "@vue/web-component-wrapper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.2.0.tgz", - "integrity": "sha512-Xn/+vdm9CjuC9p3Ae+lTClNutrVhsXpzxvoTXXtoys6kVRX9FkueSUAqSWAyZntmVLlR4DosBV4pH8y5Z/HbUw==", - "dev": true - }, - "@webassemblyjs/ast": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", - "integrity": "sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/wast-parser": "1.7.11" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz", - "integrity": "sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz", - "integrity": "sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz", - "integrity": "sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz", - "integrity": "sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.7.11" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz", - "integrity": "sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz", - "integrity": "sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==", - "dev": true - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz", - "integrity": "sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz", - "integrity": "sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz", - "integrity": "sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.11.tgz", - "integrity": "sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.1" - } - }, - "@webassemblyjs/utf8": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.11.tgz", - "integrity": "sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz", - "integrity": "sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/helper-wasm-section": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11", - "@webassemblyjs/wasm-opt": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11", - "@webassemblyjs/wast-printer": "1.7.11" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz", - "integrity": "sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/ieee754": "1.7.11", - "@webassemblyjs/leb128": "1.7.11", - "@webassemblyjs/utf8": "1.7.11" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz", - "integrity": "sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz", - "integrity": "sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-api-error": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/ieee754": "1.7.11", - "@webassemblyjs/leb128": "1.7.11", - "@webassemblyjs/utf8": "1.7.11" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz", - "integrity": "sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/floating-point-hex-parser": "1.7.11", - "@webassemblyjs/helper-api-error": "1.7.11", - "@webassemblyjs/helper-code-frame": "1.7.11", - "@webassemblyjs/helper-fsm": "1.7.11", - "@xtuc/long": "4.2.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz", - "integrity": "sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/wast-parser": "1.7.11", - "@xtuc/long": "4.2.1" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz", - "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "dev": true, - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", - "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", - "dev": true, - "requires": { - "acorn": "^5.0.0" - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true - }, - "address": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", - "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==", - "dev": true - }, - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-keywords": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", - "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", - "dev": true - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "arch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", - "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true, - "requires": { - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "autoprefixer": { - "version": "9.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.7.tgz", - "integrity": "sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA==", - "dev": true, - "requires": { - "browserslist": "^4.4.1", - "caniuse-lite": "^1.0.30000932", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^7.0.14", - "postcss-value-parser": "^3.3.1" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "babel-eslint": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", - "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "eslint-scope": "3.7.1", - "eslint-visitor-keys": "^1.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } - } - }, - "babel-loader": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.5.tgz", - "integrity": "sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==", - "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "util.promisify": "^1.0.0" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz", - "integrity": "sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bfj": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", - "integrity": "sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.1", - "check-types": "^7.3.0", - "hoopy": "^0.1.2", - "tryer": "^1.0.0" - } - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz", - "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==", - "dev": true - }, - "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "dev": true, - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - }, - "dependencies": { - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - } - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", - "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000929", - "electron-to-chromium": "^1.3.103", - "node-releases": "^1.1.3" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cacache": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", - "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", - "dev": true, - "requires": { - "bluebird": "^3.5.3", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cache-loader": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-2.0.1.tgz", - "integrity": "sha512-V99T3FOynmGx26Zom+JrVBytLBsmUCzVG2/4NnUKgvXN4bEV42R1ERl1IyiH/cvFIDA1Ytq2lPZ9tXDSahcQpQ==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.0", - "normalize-path": "^3.0.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30000936", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz", - "integrity": "sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw==", - "dev": true - }, - "case-sensitive-paths-webpack-plugin": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz", - "integrity": "sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz", - "integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg==", - "dev": true - }, - "chokidar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", - "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.0" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", - "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "clean-css": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", - "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-spinners": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", - "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", - "dev": true - }, - "cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "dev": true, - "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "clipboardy": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", - "integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==", - "dev": true, - "requires": { - "arch": "^2.1.0", - "execa": "^0.8.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.0.tgz", - "integrity": "sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "compressible": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", - "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==", - "dev": true, - "requires": { - "mime-db": ">= 1.36.0 < 2" - } - }, - "compression": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", - "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.14", - "debug": "2.6.9", - "on-headers": "~1.0.1", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "dev": true, - "requires": { - "bluebird": "^3.1.1" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-webpack-plugin": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz", - "integrity": "sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==", - "dev": true, - "requires": { - "cacache": "^10.0.4", - "find-cache-dir": "^1.0.0", - "globby": "^7.1.1", - "is-glob": "^4.0.0", - "loader-utils": "^1.1.0", - "minimatch": "^3.0.4", - "p-limit": "^1.0.0", - "serialize-javascript": "^1.4.0" - }, - "dependencies": { - "cacache": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", - "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", - "dev": true, - "requires": { - "bluebird": "^3.5.1", - "chownr": "^1.0.1", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "lru-cache": "^4.1.1", - "mississippi": "^2.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^5.2.4", - "unique-filename": "^1.1.0", - "y18n": "^4.0.0" - } - }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "mississippi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", - "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^2.0.1", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "ssri": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", - "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "core-js": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.4.tgz", - "integrity": "sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz", - "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "parse-json": "^4.0.0" - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - } - }, - "css-loader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz", - "integrity": "sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "css-selector-tokenizer": "^0.7.0", - "icss-utils": "^2.1.0", - "loader-utils": "^1.0.2", - "lodash": "^4.17.11", - "postcss": "^6.0.23", - "postcss-modules-extract-imports": "^1.2.0", - "postcss-modules-local-by-default": "^1.2.0", - "postcss-modules-scope": "^1.1.0", - "postcss-modules-values": "^1.3.0", - "postcss-value-parser": "^3.3.0", - "source-list-map": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "css-select": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", - "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^2.1.2", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-selector-tokenizer": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", - "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", - "dev": true, - "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" - }, - "dependencies": { - "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", - "dev": true - }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - } - } - }, - "css-tree": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz", - "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==", - "dev": true, - "requires": { - "mdn-data": "~1.1.0", - "source-map": "^0.5.3" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "css-unit-converter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", - "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", - "dev": true - }, - "css-url-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz", - "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=", - "dev": true - }, - "css-what": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz", - "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==", - "dev": true - }, - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "dev": true - }, - "cssnano": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.9.tgz", - "integrity": "sha512-osEbYy4kzaNY3nkd92Uf3hy5Jqb5Aztuv+Ze3Z6DjRhyntZDlb3YljiYDdJ05k167U86CZpSR+rbuJYN7N3oBQ==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.7", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "cssnano-preset-default": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", - "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.2", - "postcss-unique-selectors": "^4.0.1" - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "csso": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", - "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", - "dev": true, - "requires": { - "css-tree": "1.0.0-alpha.29" - }, - "dependencies": { - "css-tree": { - "version": "1.0.0-alpha.29", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", - "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", - "dev": true, - "requires": { - "mdn-data": "~1.1.0", - "source-map": "^0.5.3" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", - "dev": true - }, - "default-gateway": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", - "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==", - "dev": true, - "requires": { - "execa": "^0.10.0", - "ip-regex": "^2.1.0" - }, - "dependencies": { - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - } - } - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "dev": true, - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "requires": { - "utila": "~0.4" - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", - "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "easy-stack": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.0.tgz", - "integrity": "sha1-EskbMIWjfwuqM26UhurEv5Tj54g=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "ejs": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", - "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.113", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz", - "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==", - "dev": true - }, - "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "error-stack-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.2.tgz", - "integrity": "sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==", - "dev": true, - "requires": { - "stackframe": "^1.0.4" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "eslint": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.13.0.tgz", - "integrity": "sha512-nqD5WQMisciZC5EHZowejLKQjWGuFS5c70fxqSKlnDME+oz9zmE8KTlX+lHSg+/5wsC/kf9Q9eMkC8qS3oM2fg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.5.3", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^2.1.0", - "eslint-scope": "^4.0.0", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.0", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.1.0", - "js-yaml": "^3.12.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.5", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.0.2", - "text-table": "^0.2.0" - }, - "dependencies": { - "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", - "dev": true - }, - "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", - "dev": true - }, - "ansi-regex": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", - "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", - "dev": true - }, - "espree": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", - "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", - "dev": true, - "requires": { - "acorn": "^6.0.2", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", - "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - } - }, - "string-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.0.0.tgz", - "integrity": "sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", - "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", - "dev": true, - "requires": { - "ansi-regex": "^4.0.0" - } - } - } - }, - "table": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", - "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==", - "dev": true, - "requires": { - "ajv": "^6.9.1", - "lodash": "^4.17.11", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - } - } - } - }, - "eslint-loader": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.1.2.tgz", - "integrity": "sha512-rA9XiXEOilLYPOIInvVH5S/hYfyTPyxag6DZhoQOduM+3TkghAEQ3VcFO8VnX4J4qg/UIBzp72aOf/xvYmpmsg==", - "dev": true, - "requires": { - "loader-fs-cache": "^1.0.0", - "loader-utils": "^1.0.2", - "object-assign": "^4.0.1", - "object-hash": "^1.1.4", - "rimraf": "^2.6.1" - } - }, - "eslint-plugin-vue": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-5.1.0.tgz", - "integrity": "sha512-C7avvbGLb9J1PyGiFolPcGR4ljUc+dKm5ZJdrUKXwXFxHHx4SqOmRI29AsFyW7PbCGcnOvIlaq7NJS6HDIak+g==", - "dev": true, - "requires": { - "vue-eslint-parser": "^4.0.2" - }, - "dependencies": { - "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", - "dev": true - }, - "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", - "dev": true - }, - "espree": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", - "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", - "dev": true, - "requires": { - "acorn": "^6.0.2", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "vue-eslint-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-4.0.3.tgz", - "integrity": "sha512-AUeQsYdO6+7QXCems+WvGlrXd37PHv/zcRQSQdY1xdOMwdFAPEnMBsv7zPvk0TPGulXkK/5p/ITgrjiYB7k3ag==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "eslint-scope": "^4.0.0", - "eslint-visitor-keys": "^1.0.0", - "espree": "^4.1.0", - "esquery": "^1.0.1", - "lodash": "^4.17.11" - } - } - } - }, - "eslint-scope": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", - "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", - "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "event-pubsub": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", - "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", - "dev": true - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", - "dev": true - }, - "events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", - "dev": true - }, - "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", - "dev": true, - "requires": { - "original": "^1.0.0" - } - }, - "eventsource-polyfill": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/eventsource-polyfill/-/eventsource-polyfill-0.9.6.tgz", - "integrity": "sha1-EODRh/ERsWfyj9q5GIQ859gY8Tw=", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-glob": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz", - "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastparse": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", - "dev": true - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "file-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-3.0.1.tgz", - "integrity": "sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==", - "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "filesize": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", - "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "find-cache-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz", - "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - } - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", - "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", - "dev": true, - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "globals": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", - "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", - "dev": true - }, - "globby": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.0.0.tgz", - "integrity": "sha512-q0qiO/p1w/yJ0hk8V9x1UXlgsXUxlGd0AHUOXZVXBO6aznDtpx7M8D1kBrCAItoPm+4l8r6ATXV1JpjY2SBQOw==", - "dev": true, - "requires": { - "array-union": "^1.0.2", - "dir-glob": "^2.2.1", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - }, - "dependencies": { - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true - }, - "gzip-size": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz", - "integrity": "sha512-5iI7omclyqrnWw4XbXAmGhPsABkSIDQonv2K0h61lybgofWa6iZyvrI3r2zsJH4P8Nb64fFVzlvfhs0g7BBxAA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "pify": "^3.0.0" - } - }, - "handle-thing": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", - "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", - "dev": true - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoek": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", - "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==", - "dev": true - }, - "hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "dev": true - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", - "dev": true - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", - "dev": true - }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", - "dev": true - }, - "html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", - "dev": true, - "requires": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "dependencies": { - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "htmlparser2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", - "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.1", - "domutils": "1.1", - "readable-stream": "1.0" - }, - "dependencies": { - "domutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", - "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-parser-js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz", - "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==", - "dev": true - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", - "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", - "dev": true, - "requires": { - "http-proxy": "^1.16.2", - "is-glob": "^4.0.0", - "lodash": "^4.17.5", - "micromatch": "^3.1.9" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", - "dev": true - }, - "icss-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", - "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", - "dev": true, - "requires": { - "postcss": "^6.0.1" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", - "dev": true, - "requires": { - "import-from": "^2.1.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", - "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", - "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", - "dev": true - }, - "strip-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", - "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", - "dev": true, - "requires": { - "ansi-regex": "^4.0.0" - } - } - } - }, - "internal-ip": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", - "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==", - "dev": true, - "requires": { - "default-gateway": "^2.6.0", - "ipaddr.js": "^1.5.2" - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-svg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", - "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "dev": true, - "requires": { - "punycode": "2.x.x" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "javascript-stringify": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-1.6.0.tgz", - "integrity": "sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM=", - "dev": true - }, - "joi": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz", - "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==", - "dev": true, - "requires": { - "hoek": "6.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - }, - "js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "dev": true - }, - "js-message": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz", - "integrity": "sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=", - "dev": true - }, - "js-queue": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.0.tgz", - "integrity": "sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=", - "dev": true, - "requires": { - "easy-stack": "^1.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "launch-editor": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", - "integrity": "sha512-On+V7K2uZK6wK7x691ycSUbLD/FyKKelArkbaAMSSJU8JmqmhwN2+mnJDNINuJWSrh2L0kDk+ZQtbC/gOWUwLw==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "shell-quote": "^1.6.1" - } - }, - "launch-editor-middleware": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.2.1.tgz", - "integrity": "sha512-s0UO2/gEGiCgei3/2UN3SMuUj1phjQN8lcpnvgLSz26fAzNWPQ6Nf/kF5IFClnfU2ehp6LrmKdMU/beveO+2jg==", - "dev": true, - "requires": { - "launch-editor": "^2.2.1" - } - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "loader-fs-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz", - "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=", - "dev": true, - "requires": { - "find-cache-dir": "^0.1.1", - "mkdirp": "0.5.1" - }, - "dependencies": { - "find-cache-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", - "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "mkdirp": "^0.5.1", - "pkg-dir": "^1.0.0" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "^1.0.0" - } - } - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "lodash.defaultsdeep": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz", - "integrity": "sha1-vsECT4WxvZbL6kBbI8FK1kQ6b4E=", - "dev": true - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", - "dev": true - }, - "lodash.mapvalues": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", - "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.transform": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", - "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "loglevel": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", - "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", - "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", - "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - } - }, - "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", - "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", - "dev": true - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "dev": true, - "requires": { - "mime-db": "~1.37.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz", - "integrity": "sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nan": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-forge": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", - "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", - "dev": true - }, - "node-ipc": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", - "integrity": "sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==", - "dev": true, - "requires": { - "event-pubsub": "4.3.0", - "js-message": "1.0.5", - "js-queue": "2.0.0" - } - }, - "node-libs-browser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", - "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.0", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "0.0.4" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "node-releases": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.7.tgz", - "integrity": "sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==", - "dev": true, - "requires": { - "semver": "^5.3.0" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", - "dev": true - }, - "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "opener": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", - "dev": true - }, - "opn": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", - "integrity": "sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ora": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.1.0.tgz", - "integrity": "sha512-vRBPaNCclUi8pUxRF/G8+5qEQkc6EgzKK1G2ZNJUIGu088Un5qIxFXeDgymvPRM9nmrcUOGzQgS1Vmtz+NtlMw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^1.3.1", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", - "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", - "dev": true - }, - "strip-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", - "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", - "dev": true, - "requires": { - "ansi-regex": "^4.0.0" - } - } - } - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", - "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==", - "dev": true - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "pako": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", - "dev": true - }, - "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", - "dev": true, - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parent-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz", - "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", - "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==", - "dev": true - } - } - }, - "parse-asn1": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz", - "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==", - "dev": true, - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, - "portfinder": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz", - "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==", - "dev": true, - "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", - "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-calc": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", - "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", - "dev": true, - "requires": { - "css-unit-converter": "^1.1.1", - "postcss": "^7.0.5", - "postcss-selector-parser": "^5.0.0-rc.4", - "postcss-value-parser": "^3.3.1" - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-load-config": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", - "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", - "dev": true, - "requires": { - "cosmiconfig": "^4.0.0", - "import-cwd": "^2.0.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", - "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", - "dev": true, - "requires": { - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "parse-json": "^4.0.0", - "require-from-string": "^2.0.1" - } - } - } - }, - "postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", - "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==", - "dev": true, - "requires": { - "postcss": "^6.0.1" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-modules-local-by-default": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", - "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", - "dev": true, - "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-modules-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", - "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", - "dev": true, - "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-modules-values": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", - "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", - "dev": true, - "requires": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^6.0.1" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dev": true, - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "postcss-svgo": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", - "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", - "dev": true, - "requires": { - "is-svg": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prettier": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.3.tgz", - "integrity": "sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==", - "dev": true - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "dev": true, - "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz", - "integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==", - "dev": true - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", - "dev": true - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "dev": true, - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "read-pkg": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", - "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", - "dev": true, - "requires": { - "normalize-package-data": "^2.3.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", - "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", - "dev": true - }, - "regenerator-transform": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", - "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp-tree": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.1.tgz", - "integrity": "sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw==", - "dev": true, - "requires": { - "cli-table3": "^0.5.0", - "colors": "^1.1.2", - "yargs": "^12.0.5" - } - }, - "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", - "dev": true - }, - "regexpu-core": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.4.0.tgz", - "integrity": "sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^7.0.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.0.2" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", - "dev": true - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "renderkid": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.2.tgz", - "integrity": "sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg==", - "dev": true, - "requires": { - "css-select": "^1.1.0", - "dom-converter": "~0.2", - "htmlparser2": "~3.3.0", - "strip-ansi": "^3.0.0", - "utila": "^0.4.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", - "dev": true, - "requires": { - "lodash": "^4.13.1" - } - }, - "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", - "dev": true, - "requires": { - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "dev": true, - "requires": { - "rx-lite": "*" - } - }, - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz", - "integrity": "sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw==", - "dev": true, - "requires": { - "node-forge": "0.7.5" - } - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz", - "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==", - "dev": true - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz", - "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==", - "dev": true, - "requires": { - "debug": "^3.2.5", - "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - } - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", - "dev": true - }, - "spdy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", - "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stackframe": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.0.4.tgz", - "integrity": "sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - } - }, - "string.prototype.padstart": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padstart/-/string.prototype.padstart-3.0.0.tgz", - "integrity": "sha1-W8+tOfRkm7LQMSkuGbzwtRDUskI=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "stylehacks": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.2.tgz", - "integrity": "sha512-AZwvn2b3aNKK1yp+VgNPOuC2jIJOvh9PAiCq2gjDBW1WkQxQUksR1RugOJRIOhMYTGHZeoMcMQKp3/qaS3evNg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", - "dev": true - }, - "svgo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.1.1.tgz", - "integrity": "sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==", - "dev": true, - "requires": { - "coa": "~2.0.1", - "colors": "~1.1.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "~0.1.0", - "css-tree": "1.0.0-alpha.28", - "css-url-regex": "^1.1.0", - "csso": "^3.5.0", - "js-yaml": "^3.12.0", - "mkdirp": "~0.5.1", - "object.values": "^1.0.4", - "sax": "~1.2.4", - "stable": "~0.1.6", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - } - } - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - } - } - }, - "tapable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", - "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==", - "dev": true - }, - "terser": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-3.16.1.tgz", - "integrity": "sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==", - "dev": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1", - "source-map-support": "~0.5.9" - } - }, - "terser-webpack-plugin": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz", - "integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==", - "dev": true, - "requires": { - "cacache": "^11.0.2", - "find-cache-dir": "^2.0.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.4.0", - "source-map": "^0.6.1", - "terser": "^3.16.1", - "webpack-sources": "^1.1.0", - "worker-farm": "^1.5.2" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "thread-loader": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-2.1.2.tgz", - "integrity": "sha512-7xpuc9Ifg6WU+QYw/8uUqNdRwMD+N5gjwHKMqETrs96Qn+7BHwECpt2Brzr4HFlf4IAkZsayNhmGdbkBsTJ//w==", - "dev": true, - "requires": { - "loader-runner": "^2.3.1", - "loader-utils": "^1.1.0", - "neo-async": "^2.6.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", - "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", - "dev": true - }, - "timers-browserify": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", - "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "topo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", - "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", - "dev": true, - "requires": { - "hoek": "6.x.x" - } - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "dev": true - }, - "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz", - "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz", - "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==", - "dev": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", - "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-loader": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", - "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "mime": "^2.0.3", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "url-parse": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz", - "integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==", - "dev": true, - "requires": { - "querystringify": "^2.0.0", - "requires-port": "^1.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "vendors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", - "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } - }, - "vue": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.6.tgz", - "integrity": "sha512-Y2DdOZD8sxApS+iUlwv1v8U1qN41kq6Kw45lM6nVZKhygeWA49q7VCCXkjXqeDBXgurrKWkYQ9cJeEJwAq0b9Q==" - }, - "vue-eslint-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", - "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.2", - "esquery": "^1.0.0", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } - } - }, - "vue-hot-reload-api": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.2.tgz", - "integrity": "sha512-NpznMQoe/DzMG7nJjPkJKT7FdEn9xXfnntG7POfTmqnSaza97ylaBf1luZDh4IgV+vgUoR//id5pf8Ru+Ym+0g==", - "dev": true - }, - "vue-loader": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.6.2.tgz", - "integrity": "sha512-T6fONodj861M3PqZ1jlbUFjeezbUnPRY2bd+3eZuDvYADgkN3VFU2H5feqySNg9XBt8rcbyBGmFWTZtrOX+v5w==", - "dev": true, - "requires": { - "@vue/component-compiler-utils": "^2.5.1", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - } - }, - "vue-style-loader": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", - "integrity": "sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==", - "dev": true, - "requires": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "vue-template-compiler": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.6.tgz", - "integrity": "sha512-OakxDGyrmMQViCjkakQFbDZlG0NibiOzpLauOfyCUVRQc9yPmTqpiz9nF0VeA+dFkXegetw0E5x65BFhhLXO0A==", - "dev": true, - "requires": { - "de-indent": "^1.0.2", - "he": "^1.1.0" - } - }, - "vue-template-es2015-compiler": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.8.2.tgz", - "integrity": "sha512-cliV19VHLJqFUYbz/XeWXe5CO6guzwd0yrrqqp0bmjlMP3ZZULY7fu8RTC4+3lmHwo6ESVDHFDsvjB15hcR5IA==", - "dev": true - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webpack": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.28.4.tgz", - "integrity": "sha512-NxjD61WsK/a3JIdwWjtIpimmvE6UrRi3yG54/74Hk9rwNj5FPkA4DJCf1z4ByDWLkvZhTZE+P3C/eh6UD5lDcw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-module-context": "1.7.11", - "@webassemblyjs/wasm-edit": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11", - "acorn": "^5.6.2", - "acorn-dynamic-import": "^3.0.0", - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0", - "chrome-trace-event": "^1.0.0", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.0", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.3.0", - "loader-utils": "^1.1.0", - "memory-fs": "~0.4.1", - "micromatch": "^3.1.8", - "mkdirp": "~0.5.0", - "neo-async": "^2.5.0", - "node-libs-browser": "^2.0.0", - "schema-utils": "^0.4.4", - "tapable": "^1.1.0", - "terser-webpack-plugin": "^1.1.0", - "watchpack": "^1.5.0", - "webpack-sources": "^1.3.0" - } - }, - "webpack-bundle-analyzer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.4.tgz", - "integrity": "sha512-ggDUgtKuQki4vmc93Ej65GlYxeCUR/0THa7gA+iqAGC2FFAxO+r+RM9sAUa8HWdw4gJ3/NZHX/QUcVgRjdIsDg==", - "dev": true, - "requires": { - "acorn": "^5.7.3", - "bfj": "^6.1.1", - "chalk": "^2.4.1", - "commander": "^2.18.0", - "ejs": "^2.6.1", - "express": "^4.16.3", - "filesize": "^3.6.1", - "gzip-size": "^5.0.0", - "lodash": "^4.17.10", - "mkdirp": "^0.5.1", - "opener": "^1.5.1", - "ws": "^6.0.0" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true - } - } - }, - "webpack-chain": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-4.12.1.tgz", - "integrity": "sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==", - "dev": true, - "requires": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^1.6.0" - } - }, - "webpack-dev-middleware": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz", - "integrity": "sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA==", - "dev": true, - "requires": { - "memory-fs": "~0.4.1", - "mime": "^2.3.1", - "range-parser": "^1.0.3", - "webpack-log": "^2.0.0" - } - }, - "webpack-dev-server": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", - "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "bonjour": "^3.5.0", - "chokidar": "^2.0.0", - "compression": "^1.5.2", - "connect-history-api-fallback": "^1.3.0", - "debug": "^3.1.0", - "del": "^3.0.0", - "express": "^4.16.2", - "html-entities": "^1.2.0", - "http-proxy-middleware": "~0.18.0", - "import-local": "^2.0.0", - "internal-ip": "^3.0.1", - "ip": "^1.1.5", - "killable": "^1.0.0", - "loglevel": "^1.4.1", - "opn": "^5.1.0", - "portfinder": "^1.0.9", - "schema-utils": "^1.0.0", - "selfsigned": "^1.9.1", - "semver": "^5.6.0", - "serve-index": "^1.7.2", - "sockjs": "0.3.19", - "sockjs-client": "1.3.0", - "spdy": "^4.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^5.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "3.4.0", - "webpack-log": "^2.0.0", - "yargs": "12.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "dev": true, - "requires": { - "xregexp": "4.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" - } - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "webpack-hot-middleware": { - "version": "2.24.3", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz", - "integrity": "sha512-pPlmcdoR2Fn6UhYjAhp1g/IJy1Yc9hD+T6O9mjRcWV2pFbBjIFoJXhP0CoD0xPOhWJuWXuZXGBga9ybbOdzXpg==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "html-entities": "^1.2.0", - "querystring": "^0.2.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "webpack-merge": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.1.tgz", - "integrity": "sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw==", - "dev": true, - "requires": { - "lodash": "^4.17.5" - } - }, - "webpack-sources": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", - "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "worker-farm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", - "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "ws": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", - "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - } - } - }, - "yorkie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz", - "integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==", - "dev": true, - "requires": { - "execa": "^0.8.0", - "is-ci": "^1.0.10", - "normalize-path": "^1.0.0", - "strip-indent": "^2.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "normalize-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - } - } -} diff --git a/cmd/templates/vuebasic/frontend/package.json.template b/cmd/templates/vuebasic/frontend/package.json.template deleted file mode 100644 index 3b419e105..000000000 --- a/cmd/templates/vuebasic/frontend/package.json.template +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "{{.NPMProjectName}}", - "author": "{{.Author.Name}}<{{.Author.Email}}>", - "private": true, - "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "lint": "vue-cli-service lint" - }, - "dependencies": { - "core-js": "^2.6.4", - "vue": "^2.5.22" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^3.4.0", - "@vue/cli-plugin-eslint": "^3.4.0", - "@vue/cli-service": "^3.4.0", - "babel-eslint": "^10.0.1", - "eslint": "^5.8.0", - "eslint-plugin-vue": "^5.0.0", - "eventsource-polyfill": "^0.9.6", - "vue-template-compiler": "^2.5.21", - "webpack-hot-middleware": "^2.24.3" - }, - "eslintConfig": { - "root": true, - "env": { - "node": true - }, - "extends": [ - "plugin:vue/essential", - "eslint:recommended" - ], - "rules": {}, - "parserOptions": { - "parser": "babel-eslint" - } - }, - "postcss": { - "plugins": { - "autoprefixer": {} - } - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not ie <= 8" - ] -} diff --git a/cmd/templates/vuebasic/frontend/public/favicon.ico b/cmd/templates/vuebasic/frontend/public/favicon.ico deleted file mode 100644 index c7b9a43c8..000000000 Binary files a/cmd/templates/vuebasic/frontend/public/favicon.ico and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/public/index.html b/cmd/templates/vuebasic/frontend/public/index.html deleted file mode 100644 index 8adf14df5..000000000 --- a/cmd/templates/vuebasic/frontend/public/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - my-vue-app-01 - - - -
- - - diff --git a/cmd/templates/vuebasic/frontend/src/App.vue b/cmd/templates/vuebasic/frontend/src/App.vue deleted file mode 100644 index 4700f3a71..000000000 --- a/cmd/templates/vuebasic/frontend/src/App.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/cmd/templates/vuebasic/frontend/src/assets/css/main.css b/cmd/templates/vuebasic/frontend/src/assets/css/main.css deleted file mode 100644 index ae5df813d..000000000 --- a/cmd/templates/vuebasic/frontend/src/assets/css/main.css +++ /dev/null @@ -1,38 +0,0 @@ -#app { - font-family: "Roboto", Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-align: center; - color: #eee; - margin-top: 60px; -} - -html { - height: 100%; - overflow: hidden; - background-color: #131313; - background-size: 20px 20px; -} - -.logo { - width: 16em; -} - -/* roboto-regular - latin */ -@font-face { - font-family: "Roboto"; - font-style: normal; - font-weight: 400; - src: url("../fonts/roboto/roboto-v18-latin-regular.eot"); /* IE9 Compat Modes */ - src: local("Roboto"), local("Roboto-Regular"), - url("../fonts/roboto/roboto-v18-latin-regular.eot?#iefix") - format("embedded-opentype"), - /* IE6-IE8 */ url("../fonts/roboto/roboto-v18-latin-regular.woff2") - format("woff2"), - /* Super Modern Browsers */ - url("../fonts/roboto/roboto-v18-latin-regular.woff") format("woff"), - /* Modern Browsers */ url("../fonts/roboto/roboto-v18-latin-regular.ttf") - format("truetype"), - /* Safari, Android, iOS */ - url("../fonts/roboto/roboto-v18-latin-regular.svg#Roboto") format("svg"); /* Legacy iOS */ -} diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.eot b/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.eot deleted file mode 100644 index a0780d6e3..000000000 Binary files a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.eot and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.svg b/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.svg deleted file mode 100644 index 627f5a368..000000000 --- a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.svg +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.ttf b/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.ttf deleted file mode 100644 index b91bf3f7e..000000000 Binary files a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.ttf and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff b/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff deleted file mode 100644 index 92dfacc61..000000000 Binary files a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff2 b/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff2 deleted file mode 100644 index 7e854e669..000000000 Binary files a/cmd/templates/vuebasic/frontend/src/assets/fonts/roboto/roboto-v18-latin-regular.woff2 and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/src/assets/images/logo.png b/cmd/templates/vuebasic/frontend/src/assets/images/logo.png deleted file mode 100644 index 31fc8249c..000000000 Binary files a/cmd/templates/vuebasic/frontend/src/assets/images/logo.png and /dev/null differ diff --git a/cmd/templates/vuebasic/frontend/src/components/HelloWorld.vue b/cmd/templates/vuebasic/frontend/src/components/HelloWorld.vue deleted file mode 100644 index 722175f7b..000000000 --- a/cmd/templates/vuebasic/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - diff --git a/cmd/templates/vuebasic/frontend/src/main.js b/cmd/templates/vuebasic/frontend/src/main.js deleted file mode 100644 index 549d83fbb..000000000 --- a/cmd/templates/vuebasic/frontend/src/main.js +++ /dev/null @@ -1,12 +0,0 @@ -import Vue from "vue"; -import App from "./App.vue"; - -Vue.config.productionTip = false; - -import Bridge from "./wailsbridge"; - -Bridge.Start(() => { - new Vue({ - render: h => h(App) - }).$mount("#app"); -}); diff --git a/cmd/templates/vuebasic/frontend/src/wailsbridge.js b/cmd/templates/vuebasic/frontend/src/wailsbridge.js deleted file mode 100644 index 62c723900..000000000 --- a/cmd/templates/vuebasic/frontend/src/wailsbridge.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - Wails Bridge (c) 2019-present Lea Anthony - - This prod version is to get around having to rewrite your code - for production. When doing a release build, this file will be used - instead of the full version. -*/ - -export default { - // The main function - // Passes the main Wails object to the callback if given. - Start: function(callback) { - if (callback) { - window.wails.events.on("wails:ready", callback); - } - } -}; diff --git a/cmd/templates/vuebasic/frontend/vue.config.js b/cmd/templates/vuebasic/frontend/vue.config.js deleted file mode 100644 index 471d578f5..000000000 --- a/cmd/templates/vuebasic/frontend/vue.config.js +++ /dev/null @@ -1,43 +0,0 @@ -let cssConfig = {}; - -if (process.env.NODE_ENV == "production") { - cssConfig = { - extract: { - filename: "[name].css", - chunkFilename: "[name].css" - } - }; -} - -module.exports = { - chainWebpack: config => { - let limit = 9999999999999999; - config.module - .rule("images") - .test(/\.(png|gif|jpg)(\?.*)?$/i) - .use("url-loader") - .loader("url-loader") - .tap(options => Object.assign(options, { limit: limit })); - config.module - .rule("fonts") - .test(/\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/i) - .use("url-loader") - .loader("url-loader") - .options({ - limit: limit - }); - }, - css: cssConfig, - configureWebpack: { - output: { - filename: "[name].js" - }, - optimization: { - splitChunks: false - } - }, - devServer: { - disableHostCheck: true, - host: "localhost" - } -}; diff --git a/cmd/templates/vuebasic/go.mod.template b/cmd/templates/vuebasic/go.mod.template deleted file mode 100644 index 5c2676020..000000000 --- a/cmd/templates/vuebasic/go.mod.template +++ /dev/null @@ -1 +0,0 @@ -module {{.BinaryName}} \ No newline at end of file diff --git a/cmd/templates/vuebasic/main.go.template b/cmd/templates/vuebasic/main.go.template deleted file mode 100644 index 2d590fa37..000000000 --- a/cmd/templates/vuebasic/main.go.template +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "github.com/leaanthony/mewn" - "github.com/wailsapp/wails" -) - -func basic() string { - return "Hello World!" -} - -func main() { - - app := wails.CreateApp(&wails.AppConfig{ - Width: 1024, - Height: 768, - Title: "{{.Name}}", - JS: mewn.String("./frontend/dist/app.js"), - CSS: mewn.String("./frontend/dist/app.css"), - Colour: "#131313", - }) - app.Bind(basic) - app.Run() -} diff --git a/cmd/templates/vuebasic/template.json b/cmd/templates/vuebasic/template.json deleted file mode 100644 index f2d017823..000000000 --- a/cmd/templates/vuebasic/template.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Vue2/Webpack Basic", - "shortdescription": "A basic Vue2/WebPack4 template", - "description": "A basic template using Vue 2 and bundled using Webpack 4", - "author": "Lea Anthony", - "created": "2018-12-01", - "frontenddir": "frontend", - "install": "npm install", - "build": "npm run build", - "serve": "npm run serve", - "bridge": "src" -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index acbe1c74c..000000000 --- a/cmd/version.go +++ /dev/null @@ -1,5 +0,0 @@ -package cmd - -// Version - Wails version -// ...oO(There must be a better way) -const Version = "v0.9.8" diff --git a/cmd/wails/0_setup.go b/cmd/wails/0_setup.go deleted file mode 100644 index d82ff4b4e..000000000 --- a/cmd/wails/0_setup.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "fmt" - "runtime" - - "github.com/wailsapp/wails/cmd" -) - -func init() { - - commandDescription := `Sets up your local environment to develop Wails apps.` - - setupCommand := app.Command("setup", "Setup the Wails environment"). - LongDescription(commandDescription) - - app.DefaultCommand(setupCommand) - - setupCommand.Action(func() error { - - logger.PrintBanner(); - - system := cmd.NewSystemHelper() - err := system.Initialise() - if err != nil { - return err - } - - var successMessage = `Ready for take off! -Create your first project by running 'wails init'.` - if runtime.GOOS != "windows" { - successMessage = "🚀 " + successMessage - } - // Platform check - err = platformCheck() - if err != nil { - return err - } - - // Check we have a cgo capable environment - logger.Yellow("Checking for prerequisites...") - errors, err := checkRequiredPrograms() - if err != nil { - return err - } - - // Linux has library deps - errors, err = checkLibraries() - if err != nil { - return err - } - - // Check Mewn - err = cmd.CheckMewn() - if err != nil { - return err - } - - logger.White("") - - if !errors { - logger.Yellow(successMessage) - } - - return err - }) -} - -func platformCheck() error { - switch runtime.GOOS { - case "darwin": - logger.Yellow("Detected Platform: OSX") - case "windows": - logger.Yellow("Detected Platform: Windows") - case "linux": - logger.Yellow("Detected Platform: Linux") - default: - return fmt.Errorf("Platform %s is currently not supported", runtime.GOOS) - } - return nil -} - -func checkLibraries() (errors bool, err error) { - if runtime.GOOS == "linux" { - // Check library prerequisites - requiredLibraries, err := cmd.GetRequiredLibraries() - if err != nil { - return false, err - } - distroInfo := cmd.GetLinuxDistroInfo() - for _, library := range *requiredLibraries { - switch distroInfo.Distribution { - case cmd.Ubuntu: - installed, err := cmd.DpkgInstalled(library.Name) - if err != nil { - return false, err - } - if !installed { - errors = true - logger.Red("Library '%s' not found. %s", library.Name, library.Help) - } else { - logger.Green("Library '%s' installed.", library.Name) - } - default: - return false, fmt.Errorf("unable to check libraries on distribution '%s'. Please ensure that the '%s' equivalent is installed", distroInfo.DistributorID, library.Name) - } - } - } - return false, nil -} - -func checkRequiredPrograms() (errors bool, err error) { - requiredPrograms, err := cmd.GetRequiredPrograms() - if err != nil { - return true, err - } - errors = false - programHelper := cmd.NewProgramHelper() - for _, program := range *requiredPrograms { - bin := programHelper.FindProgram(program.Name) - if bin == nil { - errors = true - logger.Red("Program '%s' not found. %s", program.Name, program.Help) - } else { - logger.Green("Program '%s' found: %s", program.Name, bin.Path) - } - } - return -} diff --git a/cmd/wails/2_init.go b/cmd/wails/2_init.go deleted file mode 100644 index 14583ff90..000000000 --- a/cmd/wails/2_init.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/wailsapp/wails/cmd" -) - -func init() { - - projectHelper := cmd.NewProjectHelper() - projectOptions := projectHelper.NewProjectOptions() - commandDescription := `Generates a new Wails project using the given flags. -Any flags that are required and not given will be prompted for.` - - initCommand := app.Command("init", "Initialises a new Wails project"). - LongDescription(commandDescription). - BoolFlag("f", "Use defaults", &projectOptions.UseDefaults). - StringFlag("dir", "Directory to create project in", &projectOptions.OutputDirectory). - // StringFlag("template", "Template name", &projectOptions.Template). - StringFlag("name", "Project name", &projectOptions.Name). - StringFlag("description", "Project description", &projectOptions.Description). - StringFlag("output", "Output binary name", &projectOptions.BinaryName) - - initCommand.Action(func() error { - - logger.PrintSmallBanner("Initialising project") - fmt.Println() - - // Check if the system is initialised - system := cmd.NewSystemHelper() - err := system.CheckInitialised() - if err != nil { - return err - } - - success, err := cmd.CheckDependenciesSilent(logger) - if !success { - return err - } - - // Do we want to just force defaults? - if projectOptions.UseDefaults { - // Use defaults - projectOptions.Defaults() - } else { - err = projectOptions.PromptForInputs() - if err != nil { - return err - } - } - - // Generate the project - err = projectHelper.GenerateProject(projectOptions) - if err != nil { - logger.Error(err.Error()) - } - return err - }) -} diff --git a/cmd/wails/4_build.go b/cmd/wails/4_build.go deleted file mode 100644 index 1fbaa439e..000000000 --- a/cmd/wails/4_build.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/leaanthony/spinner" - "github.com/wailsapp/wails/cmd" -) - -func init() { - - var packageApp = false - var forceRebuild = false - var debugMode = false - buildSpinner := spinner.NewSpinner() - buildSpinner.SetSpinSpeed(50) - - commandDescription := `This command will check to ensure all pre-requistes are installed prior to building. If not, it will attempt to install them. Building comprises of a number of steps: install frontend dependencies, build frontend, pack frontend, compile main application.` - initCmd := app.Command("build", "Builds your Wails project"). - LongDescription(commandDescription). - BoolFlag("p", "Package application on successful build", &packageApp). - BoolFlag("f", "Force rebuild of application components", &forceRebuild). - BoolFlag("d", "Build in Debug mode", &debugMode) - - initCmd.Action(func() error { - - message := "Building Application" - if forceRebuild { - message += " (force rebuild)" - } - logger.PrintSmallBanner(message) - fmt.Println() - - // Project options - projectOptions := &cmd.ProjectOptions{} - - // Check we are in project directory - // Check project.json loads correctly - fs := cmd.NewFSHelper() - err := projectOptions.LoadConfig(fs.Cwd()) - if err != nil { - return err - } - - // Validate config - // Check if we have a frontend - err = cmd.ValidateFrontendConfig(projectOptions) - if err != nil { - return err - } - - // Program checker - program := cmd.NewProgramHelper() - - if projectOptions.FrontEnd != nil { - // npm - if !program.IsInstalled("npm") { - return fmt.Errorf("it appears npm is not installed. Please install and run again") - } - } - - // Save project directory - projectDir := fs.Cwd() - - // Install deps - if projectOptions.FrontEnd != nil { - err = cmd.InstallFrontendDeps(projectDir, projectOptions, forceRebuild, "build") - if err != nil { - return err - } - } - - // Move to project directory - err = os.Chdir(projectDir) - if err != nil { - return err - } - - // Install dependencies - err = cmd.InstallGoDependencies() - if err != nil { - return err - } - - // Build application - buildMode := cmd.BuildModeProd - if debugMode { - buildMode = cmd.BuildModeDebug - } - - err = cmd.BuildApplication(projectOptions.BinaryName, forceRebuild, buildMode, packageApp, projectOptions) - if err != nil { - return err - } - - logger.Yellow("Awesome! Project '%s' built!", projectOptions.Name) - - return nil - - }) -} diff --git a/cmd/wails/6_serve.go b/cmd/wails/6_serve.go deleted file mode 100644 index 7be86582a..000000000 --- a/cmd/wails/6_serve.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/leaanthony/spinner" - "github.com/wailsapp/wails/cmd" -) - -func init() { - - var forceRebuild = false - buildSpinner := spinner.NewSpinner() - buildSpinner.SetSpinSpeed(50) - - commandDescription := `This command builds then serves your application in bridge mode. Useful for developing your app in a browser.` - initCmd := app.Command("serve", "Run your Wails project in bridge mode."). - LongDescription(commandDescription). - BoolFlag("f", "Force rebuild of application components", &forceRebuild) - - initCmd.Action(func() error { - - message := "Serving Application" - logger.PrintSmallBanner(message) - fmt.Println() - - // Project options - projectOptions := &cmd.ProjectOptions{} - - // Check we are in project directory - // Check project.json loads correctly - fs := cmd.NewFSHelper() - err := projectOptions.LoadConfig(fs.Cwd()) - if err != nil { - return err - } - - // Check Mewn is installed - err = cmd.CheckMewn() - if err != nil { - return err - } - - // Install dependencies - err = cmd.InstallGoDependencies() - if err != nil { - return err - } - - buildMode := cmd.BuildModeBridge - err = cmd.BuildApplication(projectOptions.BinaryName, forceRebuild, buildMode, false, projectOptions) - if err != nil { - return err - } - - logger.Yellow("Awesome! Project '%s' built!", projectOptions.Name) - return cmd.ServeProject(projectOptions, logger) - }) -} diff --git a/cmd/wails/8_update.go b/cmd/wails/8_update.go deleted file mode 100644 index a463049f6..000000000 --- a/cmd/wails/8_update.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "io/ioutil" - "encoding/json" - - "github.com/leaanthony/spinner" - "github.com/wailsapp/wails/cmd" -) - -func init() { - - // var forceRebuild = false - checkSpinner := spinner.NewSpinner() - checkSpinner.SetSpinSpeed(50) - - commandDescription := `This command checks if there are updates to Wails.` - updateCmd := app.Command("update", "Check for Updates."). - LongDescription(commandDescription) - - updateCmd.Action(func() error { - - message := "Checking for updates..." - logger.PrintSmallBanner(message) - fmt.Println() - - // Get versions - checkSpinner.Start(message) - resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags") - if err != nil { - checkSpinner.Error(err.Error()) - return err - } - checkSpinner.Success() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - checkSpinner.Error(err.Error()) - return err - } - - data := []map[string]interface{}{} - err = json.Unmarshal(body, &data) - if err != nil { - return err - } - - latestVersion := data[0]["name"].(string) - fmt.Println() - fmt.Println("Current Version: " + cmd.Version) - fmt.Println(" Latest Version: " + latestVersion) - if latestVersion != cmd.Version { - updateSpinner := spinner.NewSpinner() - updateSpinner.SetSpinSpeed(40) - updateSpinner.Start("Updating to : " + latestVersion) - err = cmd.NewProgramHelper().RunCommandArray([]string{"go","get","-u","github.com/wailsapp/wails/cmd/wails"}) - if err != nil { - updateSpinner.Error(err.Error()) - return err - } - updateSpinner.Success() - logger.Yellow("Wails updated to " + latestVersion) - } else { - logger.Yellow("Looks like you're up to date!") - } - return nil - }) -} diff --git a/cmd/wails/9_issue.go b/cmd/wails/9_issue.go deleted file mode 100644 index 6be68199c..000000000 --- a/cmd/wails/9_issue.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "runtime" - "strings" - - "github.com/pkg/browser" - - "github.com/wailsapp/wails/cmd" -) - -func init() { - - commandDescription := `Generates an issue in Github using the given title, description and system report.` - - initCommand := app.Command("issue", "Generates an issue in Github."). - LongDescription(commandDescription) - - initCommand.Action(func() error { - - logger.PrintSmallBanner("Generate Issue") - fmt.Println() - message := `Thanks for taking the time to submit an issue! - -To help you in this process, we will ask for some information, add Go/Wails details automatically, then prepare the issue for your editing and submission. -` - - logger.Yellow(message) - - title := cmd.Prompt("Issue Title") - description := cmd.Prompt("Issue Description") - - var str strings.Builder - - gomodule, exists := os.LookupEnv("GO111MODULE") - if !exists { - gomodule = "(Not Set)" - } - - str.WriteString("\n| Name | Value |\n| ----- | ----- |\n") - str.WriteString(fmt.Sprintf("| Wails Version | %s |\n", cmd.Version)) - str.WriteString(fmt.Sprintf("| Go Version | %s |\n", runtime.Version())) - str.WriteString(fmt.Sprintf("| Platform | %s |\n", runtime.GOOS)) - str.WriteString(fmt.Sprintf("| Arch | %s |\n", runtime.GOARCH)) - str.WriteString(fmt.Sprintf("| GO111MODULE | %s |\n", gomodule)) - - fmt.Println() - fmt.Println("Processing template and preparing for upload.") - - // Grab issue template - resp, err := http.Get("https://raw.githubusercontent.com/wailsapp/wails/master/.github/ISSUE_TEMPLATE/bug_report.md") - if err != nil { - logger.Red("Unable to read in issue template. Are you online?") - os.Exit(1) - } - defer resp.Body.Close() - template, _ := ioutil.ReadAll(resp.Body) - body := string(template) - body = "**Description**\n" + (strings.Split(body, "**Description**")[1]) - fullURL := "https://github.com/wailsapp/wails/issues/new?" - body = strings.Replace(body, "A clear and concise description of what the bug is.", description, -1) - body = strings.Replace(body, "Please paste the output of `wails report` here.", str.String(), -1) - params := "title=" + title + "&body=" + body - - fmt.Println("Opening browser to file issue.") - browser.OpenURL(fullURL + url.PathEscape(params)) - return nil - }) -} diff --git a/cmd/wails/main.go b/cmd/wails/main.go deleted file mode 100644 index ae562209b..000000000 --- a/cmd/wails/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "github.com/wailsapp/wails/cmd" -) - -// Create Logger -var logger = cmd.NewLogger() - -// Create main app -var app = cmd.NewCli("wails", "A cli tool for building Wails applications.") - -// Main! -func main() { - err := app.Run() - if err != nil { - logger.Error(err.Error()) - } -} diff --git a/event_manager.go b/event_manager.go deleted file mode 100644 index 6520ee840..000000000 --- a/event_manager.go +++ /dev/null @@ -1,148 +0,0 @@ -package wails - -import ( - "fmt" - "sync" -) - -// eventManager handles and processes events -type eventManager struct { - incomingEvents chan *eventData - listeners map[string][]*eventListener - exit bool - log *CustomLogger - renderer Renderer // Messages will be dispatched to the frontend -} - -// newEventManager creates a new event manager with a 100 event buffer -func newEventManager() *eventManager { - return &eventManager{ - incomingEvents: make(chan *eventData, 100), - listeners: make(map[string][]*eventListener), - exit: false, - log: newCustomLogger("Events"), - } -} - -// PushEvent places the given event on to the event queue -func (e *eventManager) PushEvent(eventData *eventData) { - e.incomingEvents <- eventData -} - -// eventListener holds a callback function which is invoked when -// the event listened for is emitted. It has a counter which indicates -// how the total number of events it is interested in. A value of zero -// means it does not expire (default). -type eventListener struct { - callback func(...interface{}) // Function to call with emitted event data - counter int // Expire after counter callbacks. 0 = infinite - expired bool // Indicates if the listener has expired -} - -// Creates a new event listener from the given callback function -func (e *eventManager) addEventListener(eventName string, callback func(...interface{}), counter int) error { - - // Sanity check inputs - if callback == nil { - return fmt.Errorf("nil callback bassed to addEventListener") - } - - // Check event has been registered before - if e.listeners[eventName] == nil { - e.listeners[eventName] = []*eventListener{} - } - - // Create the callback - listener := &eventListener{ - callback: callback, - counter: counter, - } - - // Register listener - e.listeners[eventName] = append(e.listeners[eventName], listener) - - // All good mate - return nil -} - -func (e *eventManager) On(eventName string, callback func(...interface{})) { - // Add a persistent eventListener (counter = 0) - e.addEventListener(eventName, callback, 0) -} - -// Emit broadcasts the given event to the subscribed listeners -func (e *eventManager) Emit(eventName string, optionalData ...interface{}) { - e.incomingEvents <- &eventData{Name: eventName, Data: optionalData} -} - -// Starts the event manager's queue processing -func (e *eventManager) start(renderer Renderer) { - - e.log.Info("Starting") - - // Store renderer - e.renderer = renderer - - // Set up waitgroup so we can wait for goroutine to start - var wg sync.WaitGroup - wg.Add(1) - - // Run main loop in seperate goroutine - go func() { - wg.Done() - e.log.Info("Listening") - for e.exit == false { - // TODO: Listen for application exit - select { - case event := <-e.incomingEvents: - e.log.DebugFields("Got Event", Fields{ - "data": event.Data, - "name": event.Name, - }) - - // Notify renderer - e.renderer.NotifyEvent(event) - - // Notify Go listeners - var listenersToRemove []*eventListener - - // Iterate listeners - for _, listener := range e.listeners[event.Name] { - - // Call listener, perhaps with data - if event.Data == nil { - go listener.callback() - } else { - unpacked := event.Data.([]interface{}) - go listener.callback(unpacked...) - } - - // Update listen counter - if listener.counter > 0 { - listener.counter = listener.counter - 1 - if listener.counter == 0 { - listener.expired = true - } - } - } - - // Remove expired listners in place - if len(listenersToRemove) > 0 { - listeners := e.listeners[event.Name][:0] - for _, listener := range listeners { - if !listener.expired { - listeners = append(listeners, listener) - } - } - } - } - } - }() - - // Wait for goroutine to start - wg.Wait() -} - -func (e *eventManager) stop() { - e.exit = true -} diff --git a/go.mod b/go.mod deleted file mode 100644 index a371a897d..000000000 --- a/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module github.com/wailsapp/wails - -require ( - github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc // indirect - github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac - github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947 // indirect - github.com/fatih/color v1.7.0 - github.com/go-playground/colors v1.2.0 - github.com/gorilla/websocket v1.4.0 - github.com/jackmordaunt/icns v1.0.0 - github.com/leaanthony/mewn v0.9.4 - github.com/leaanthony/slicer v1.2.0 - github.com/leaanthony/spinner v0.5.0 - github.com/mattn/go-colorable v0.1.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 - github.com/pkg/errors v0.8.1 // indirect - github.com/sirupsen/logrus v1.3.0 - github.com/stretchr/testify v1.3.0 // indirect - github.com/wailsapp/webview v0.2.5 - golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 // indirect - golang.org/x/net v0.0.0-20190213061140-3a22650c66bd // indirect - golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 78979089b..000000000 --- a/go.sum +++ /dev/null @@ -1,74 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc h1:VBS1z48BFEe00G81z8MKOtwX7f/ISkuH38NscT8iVPw= -github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc/go.mod h1:ABJPuor7YlcsHmvJ1QxX38e2NcufLY3hm0yXv+cy9sI= -github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac h1:DpMwFluHWoZpV9ex5XjkWO4HyCz5HLVI8XbHw0FhHi4= -github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac/go.mod h1:XsAE+b4rOZc8gvgsgF+wU75mNBvBcyED1wdd9PBLlJ0= -github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947 h1:Fm10/KNuoAyBm2P5P5H91Xy21hGcZnBdjR+cMdytv1M= -github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/go-playground/colors v1.2.0 h1:0EdjTXKrr2g1L/LQTYtIqabeHpZuGZz1U4osS1T8+5M= -github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= -github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/leaanthony/mewn v0.9.4 h1:thDAdV8DkCwqHcFLnfMLQtTHqK1N8leF7sD/SPsLMHI= -github.com/leaanthony/mewn v0.9.4/go.mod h1:CRkTx8unLiSSilu/Sd7i1LwrdaAL+3eQ3ses99qGMEQ= -github.com/leaanthony/slicer v1.2.0 h1:XZ+1l7cCO36j238iv5ZXkJAcM3hPtD0xb41/WE+QmuU= -github.com/leaanthony/slicer v1.2.0/go.mod h1:VMB/HGvr3uR3MRpFWHWAm0w+DHQLzPHYe2pKfpFlQIQ= -github.com/leaanthony/spinner v0.5.0 h1:HQykt/iTy7fmINEREtRbWrt+8j4MxC8dtvWBxEWM9oA= -github.com/leaanthony/spinner v0.5.0/go.mod h1:8TSFz9SL1AUC4XSbEFYE6SfN5Mlus51qYluVGrie9ww= -github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= -github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= -github.com/leaanthony/wincursor v0.1.0 h1:Dsyp68QcF5cCs65AMBmxoYNEm0n8K7mMchG6a8fYxf8= -github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= -github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/ribice/glice v0.0.0-20181011133736-685f13fa9b12/go.mod h1:A+ednilkKNW0CJGLsrLkq0D49M4EhlCi8gvnkwoZFn0= -github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/wailsapp/webview v0.2.5 h1:/VacryPEUeMBb2VHHOjpoIze6ki8tW3qYX04MnI0b7o= -github.com/wailsapp/webview v0.2.5/go.mod h1:XO9HJbKWokDxUYTWQEBCYg95n/To1v7PxvanDNVf8hY= -github.com/zserge/webview v0.0.0-20190123072648-16c93bcaeaeb/go.mod h1:a1CV8KR4Dd1eP2g+mEijGOp+HKczwdKHWyx0aPHKvo4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 h1:NwxKRvbkH5MsNkvOtPZi3/3kmI8CAzs3mtv+GLQMkNo= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30= -golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/ipc_call.go b/ipc_call.go deleted file mode 100644 index 4baa224f1..000000000 --- a/ipc_call.go +++ /dev/null @@ -1,38 +0,0 @@ -package wails - -import ( - "fmt" -) - -type callData struct { - BindingName string `json:"bindingName"` - Data string `json:"data,omitempty"` -} - -func init() { - messageProcessors["call"] = processCallData -} - -func processCallData(message *ipcMessage) (*ipcMessage, error) { - - var payload callData - - // Decode binding call data - payloadMap := message.Payload.(map[string]interface{}) - - // Check for binding name - if payloadMap["bindingName"] == nil { - return nil, fmt.Errorf("bindingName not given in call") - } - payload.BindingName = payloadMap["bindingName"].(string) - - // Check for data - if payloadMap["data"] != nil { - payload.Data = payloadMap["data"].(string) - } - - // Reassign payload to decoded data - message.Payload = &payload - - return message, nil -} diff --git a/ipc_event.go b/ipc_event.go deleted file mode 100644 index 416b160ef..000000000 --- a/ipc_event.go +++ /dev/null @@ -1,40 +0,0 @@ -package wails - -import ( - "encoding/json" -) - -type eventData struct { - Name string `json:"name"` - Data interface{} `json:"data"` -} - -// Register the message handler -func init() { - messageProcessors["event"] = processEventData -} - -// This processes the given event message -func processEventData(message *ipcMessage) (*ipcMessage, error) { - - // TODO: Is it worth double checking this is actually an event message, - // even though that's done by the caller? - var payload eventData - - // Decode event data - payloadMap := message.Payload.(map[string]interface{}) - payload.Name = payloadMap["name"].(string) - - // decode the payload data - var data []interface{} - err := json.Unmarshal([]byte(payloadMap["data"].(string)), &data) - if err != nil { - return nil, err - } - payload.Data = data - - // Reassign payload to decoded data - message.Payload = &payload - - return message, nil -} diff --git a/ipc_log.go b/ipc_log.go deleted file mode 100644 index ba35104fc..000000000 --- a/ipc_log.go +++ /dev/null @@ -1,27 +0,0 @@ -package wails - -type logData struct { - Level string `json:"level"` - Message string `json:"string"` -} - -// Register the message handler -func init() { - messageProcessors["log"] = processLogData -} - -// This processes the given log message -func processLogData(message *ipcMessage) (*ipcMessage, error) { - - var payload logData - - // Decode event data - payloadMap := message.Payload.(map[string]interface{}) - payload.Level = payloadMap["level"].(string) - payload.Message = payloadMap["message"].(string) - - // Reassign payload to decoded data - message.Payload = &payload - - return message, nil -} diff --git a/ipc_manager.go b/ipc_manager.go deleted file mode 100644 index 430c2b1ce..000000000 --- a/ipc_manager.go +++ /dev/null @@ -1,162 +0,0 @@ -package wails - -import ( - "fmt" -) - -type ipcManager struct { - renderer Renderer // The renderer - messageQueue chan *ipcMessage - // quitChannel chan struct{} - // signals chan os.Signal - log *CustomLogger - eventManager *eventManager - bindingManager *bindingManager -} - -func newIPCManager() *ipcManager { - result := &ipcManager{ - messageQueue: make(chan *ipcMessage, 100), - // quitChannel: make(chan struct{}), - // signals: make(chan os.Signal, 1), - log: newCustomLogger("IPC"), - } - return result -} - -// Sets the renderer, returns the dispatch function -func (i *ipcManager) bindRenderer(renderer Renderer) { - i.renderer = renderer -} - -func (i *ipcManager) start(eventManager *eventManager, bindingManager *bindingManager) { - - // Store manager references - i.eventManager = eventManager - i.bindingManager = bindingManager - - i.log.Info("Starting") - // signal.Notify(manager.signals, os.Interrupt) - go func() { - running := true - for running { - select { - case incomingMessage := <-i.messageQueue: - i.log.DebugFields("Processing message", Fields{ - "1D": &incomingMessage, - }) - switch incomingMessage.Type { - case "call": - callData := incomingMessage.Payload.(*callData) - i.log.DebugFields("Processing call", Fields{ - "1D": &incomingMessage, - "bindingName": callData.BindingName, - "data": callData.Data, - }) - go func() { - result, err := bindingManager.processCall(callData) - i.log.DebugFields("processed call", Fields{"result": result, "err": err}) - if err != nil { - incomingMessage.ReturnError(err.Error()) - } else { - incomingMessage.ReturnSuccess(result) - } - i.log.DebugFields("Finished processing call", Fields{ - "1D": &incomingMessage, - }) - }() - case "event": - - // Extract event data - eventData := incomingMessage.Payload.(*eventData) - - // Log - i.log.DebugFields("Processing event", Fields{ - "name": eventData.Name, - "data": eventData.Data, - }) - - // Push the event to the event manager - i.eventManager.PushEvent(eventData) - - // Log - i.log.DebugFields("Finished processing event", Fields{ - "name": eventData.Name, - }) - case "log": - logdata := incomingMessage.Payload.(*logData) - switch logdata.Level { - case "info": - logger.Info(logdata.Message) - case "debug": - logger.Debug(logdata.Message) - case "warning": - logger.Warning(logdata.Message) - case "error": - logger.Error(logdata.Message) - case "fatal": - logger.Fatal(logdata.Message) - default: - i.log.ErrorFields("Invalid log level sent", Fields{ - "level": logdata.Level, - "message": logdata.Message, - }) - } - default: - i.log.Debugf("bad message sent to MessageQueue! Unknown type: %s", incomingMessage.Type) - } - - // Log - i.log.DebugFields("Finished processing message", Fields{ - "1D": &incomingMessage, - }) - // case <-manager.quitChannel: - // Debug("[MessageQueue] Quit caught") - // running = false - // case <-manager.signals: - // Debug("[MessageQueue] Signal caught") - // running = false - } - } - i.log.Debug("Stopping") - }() -} - -// Dispatch receives JSON encoded messages from the renderer. -// It processes the message to ensure that it is valid and places -// the processed message on the message queue -func (i *ipcManager) Dispatch(message string) { - - // Create a new IPC Message - incomingMessage, err := newIPCMessage(message, i.SendResponse) - if err != nil { - i.log.ErrorFields("Could not understand incoming message! ", map[string]interface{}{ - "message": message, - "error": err, - }) - return - } - - // Put message on queue - i.log.DebugFields("Message received", map[string]interface{}{ - "type": incomingMessage.Type, - "payload": incomingMessage.Payload, - }) - - // Put incoming message on the message queue - i.messageQueue <- incomingMessage -} - -// SendResponse sends the given response back to the frontend -func (i *ipcManager) SendResponse(response *ipcResponse) error { - - // Serialise the Message - data, err := response.Serialise() - if err != nil { - fmt.Printf(err.Error()) - return err - } - - // Call back to the front end - return i.renderer.Callback(data) -} diff --git a/ipc_message.go b/ipc_message.go deleted file mode 100644 index 5d792bc59..000000000 --- a/ipc_message.go +++ /dev/null @@ -1,93 +0,0 @@ -package wails - -import ( - "encoding/json" - "fmt" -) - -// Message handler -type messageProcessorFunc func(*ipcMessage) (*ipcMessage, error) - -var messageProcessors = make(map[string]messageProcessorFunc) - -// ipcMessage is the struct version of the Message sent from the frontend. -// The payload has the specialised message data -type ipcMessage struct { - Type string `json:"type"` - Payload interface{} `json:"payload"` - CallbackID string `json:"callbackid,omitempty"` - sendResponse func(*ipcResponse) error -} - -func parseMessage(incomingMessage string) (*ipcMessage, error) { - // Parse message - var message ipcMessage - err := json.Unmarshal([]byte(incomingMessage), &message) - return &message, err -} - -func newIPCMessage(incomingMessage string, responseFunction func(*ipcResponse) error) (*ipcMessage, error) { - - // Parse the Message - message, err := parseMessage(incomingMessage) - if err != nil { - return nil, err - } - - // Check message type is valid - messageProcessor := messageProcessors[message.Type] - if messageProcessor == nil { - return nil, fmt.Errorf("unknown message type: %s", message.Type) - } - - // Process message payload - message, err = messageProcessor(message) - if err != nil { - return nil, err - } - - // Set the response function - message.sendResponse = responseFunction - - return message, nil -} - -// hasCallbackID checks if the message can send an error back to the frontend -func (m *ipcMessage) hasCallbackID() error { - if m.CallbackID == "" { - return fmt.Errorf("attempted to return error to message with no Callback ID") - } - return nil -} - -// ReturnError returns an error back to the frontend -func (m *ipcMessage) ReturnError(format string, args ...interface{}) error { - - // Ignore ReturnError if no callback ID given - err := m.hasCallbackID() - if err != nil { - return err - } - - // Create response - response := newErrorResponse(m.CallbackID, fmt.Sprintf(format, args...)) - - // Send response - return m.sendResponse(response) -} - -// ReturnSuccess returns a success message back with the given data -func (m *ipcMessage) ReturnSuccess(data interface{}) error { - - // Ignore ReturnSuccess if no callback ID given - err := m.hasCallbackID() - if err != nil { - return err - } - - // Create the response - response := newSuccessResponse(m.CallbackID, data) - - // Send response - return m.sendResponse(response) -} diff --git a/ipc_response.go b/ipc_response.go deleted file mode 100644 index 945819719..000000000 --- a/ipc_response.go +++ /dev/null @@ -1,43 +0,0 @@ -package wails - -import ( - "encoding/json" - "strings" -) - -// ipcResponse contains the response data from an RPC call -type ipcResponse struct { - CallbackID string `json:"callbackid"` - ErrorMessage string `json:"error,omitempty"` - Data interface{} `json:"data,omitempty"` -} - -// newErrorResponse returns the given error message to the frontend with the callbackid -func newErrorResponse(callbackID string, errorMessage string) *ipcResponse { - // Create response object - result := &ipcResponse{ - CallbackID: callbackID, - ErrorMessage: errorMessage, - } - return result -} - -// newSuccessResponse returns the given data to the frontend with the callbackid -func newSuccessResponse(callbackID string, data interface{}) *ipcResponse { - - // Create response object - result := &ipcResponse{ - CallbackID: callbackID, - Data: data, - } - - return result -} - -// Serialise formats the response to a string -func (i *ipcResponse) Serialise() (string, error) { - b, err := json.Marshal(i) - result := strings.Replace(string(b), "\\", "\\\\", -1) - result = strings.Replace(result, "'", "\\'", -1) - return result, err -} diff --git a/licenses/github.com/dchest/cssmin/LICENSE b/licenses/github.com/dchest/cssmin/LICENSE deleted file mode 100644 index c9b4ec5dd..000000000 --- a/licenses/github.com/dchest/cssmin/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -Go Port: -Copyright (c) 2013 Dmitry Chestnykh - -Original: -Copyright (c) 2008 Ryan Grove -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of this project nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/github.com/dchest/htmlmin/LICENSE b/licenses/github.com/dchest/htmlmin/LICENSE deleted file mode 100644 index 9ddea1fd3..000000000 --- a/licenses/github.com/dchest/htmlmin/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2013 Dmitry Chestnykh. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/github.com/fatih/color/LICENSE.md b/licenses/github.com/fatih/color/LICENSE.md deleted file mode 100644 index 25fdaf639..000000000 --- a/licenses/github.com/fatih/color/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Fatih Arslan - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/github.com/go-playground/colors/LICENSE b/licenses/github.com/go-playground/colors/LICENSE deleted file mode 100644 index 6a2ae9aa4..000000000 --- a/licenses/github.com/go-playground/colors/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Dean Karn - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/licenses/github.com/gorilla/websocket/LICENSE b/licenses/github.com/gorilla/websocket/LICENSE deleted file mode 100644 index 9171c9722..000000000 --- a/licenses/github.com/gorilla/websocket/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/github.com/konsorten/go-windows-terminal-sequences/LICENSE b/licenses/github.com/konsorten/go-windows-terminal-sequences/LICENSE deleted file mode 100644 index 14127cd83..000000000 --- a/licenses/github.com/konsorten/go-windows-terminal-sequences/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -(The MIT License) - -Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/github.com/leaanthony/mewn/LICENSE b/licenses/github.com/leaanthony/mewn/LICENSE deleted file mode 100644 index 1cf398a90..000000000 --- a/licenses/github.com/leaanthony/mewn/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -© 2019-Present Lea Anthony - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/github.com/leaanthony/spinner/LICENSE b/licenses/github.com/leaanthony/spinner/LICENSE deleted file mode 100644 index 7e01c45c9..000000000 --- a/licenses/github.com/leaanthony/spinner/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018 Lea Anthony - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/github.com/leaanthony/synx/LICENSE b/licenses/github.com/leaanthony/synx/LICENSE deleted file mode 100644 index 75ba4230d..000000000 --- a/licenses/github.com/leaanthony/synx/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018-Present Lea Anthony - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/licenses/github.com/leaanthony/wincursor/LICENSE b/licenses/github.com/leaanthony/wincursor/LICENSE deleted file mode 100644 index 75ba4230d..000000000 --- a/licenses/github.com/leaanthony/wincursor/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018-Present Lea Anthony - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/licenses/github.com/mattn/go-colorable/LICENSE b/licenses/github.com/mattn/go-colorable/LICENSE deleted file mode 100644 index 91b5cef30..000000000 --- a/licenses/github.com/mattn/go-colorable/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Yasuhiro Matsumoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/licenses/github.com/mattn/go-isatty/LICENSE b/licenses/github.com/mattn/go-isatty/LICENSE deleted file mode 100644 index 65dc692b6..000000000 --- a/licenses/github.com/mattn/go-isatty/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) Yasuhiro MATSUMOTO - -MIT License (Expat) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/github.com/mitchellh/go-homedir/LICENSE b/licenses/github.com/mitchellh/go-homedir/LICENSE deleted file mode 100644 index f9c841a51..000000000 --- a/licenses/github.com/mitchellh/go-homedir/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Mitchell Hashimoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/licenses/github.com/nfnt/resize/LICENSE b/licenses/github.com/nfnt/resize/LICENSE deleted file mode 100644 index 7836cad5f..000000000 --- a/licenses/github.com/nfnt/resize/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright (c) 2012, Jan Schlicht - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. diff --git a/licenses/github.com/pkg/errors/LICENSE b/licenses/github.com/pkg/errors/LICENSE deleted file mode 100644 index 835ba3e75..000000000 --- a/licenses/github.com/pkg/errors/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2015, Dave Cheney -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/github.com/sirupsen/logrus/LICENSE b/licenses/github.com/sirupsen/logrus/LICENSE deleted file mode 100644 index f090cb42f..000000000 --- a/licenses/github.com/sirupsen/logrus/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Simon Eskildsen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/licenses/github.com/wailsapp/webview/LICENSE b/licenses/github.com/wailsapp/webview/LICENSE deleted file mode 100644 index b18604bf4..000000000 --- a/licenses/github.com/wailsapp/webview/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Serge Zaitsev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/licenses/golang.org/x/crypto/LICENSE b/licenses/golang.org/x/crypto/LICENSE deleted file mode 100644 index 6a66aea5e..000000000 --- a/licenses/golang.org/x/crypto/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/golang.org/x/net/LICENSE b/licenses/golang.org/x/net/LICENSE deleted file mode 100644 index 6a66aea5e..000000000 --- a/licenses/golang.org/x/net/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/golang.org/x/sys/LICENSE b/licenses/golang.org/x/sys/LICENSE deleted file mode 100644 index 6a66aea5e..000000000 --- a/licenses/golang.org/x/sys/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/log.go b/log.go deleted file mode 100644 index fb8f7952e..000000000 --- a/log.go +++ /dev/null @@ -1,42 +0,0 @@ -package wails - -import ( - "os" - "strings" - - log "github.com/sirupsen/logrus" -) - -// Global logger reference -var logger = log.New() - -// Fields is used by the customLogger object to output -// fields along with a message -type Fields map[string]interface{} - -// Default options for the global logger -func init() { - logger.SetOutput(os.Stdout) - logger.SetLevel(log.DebugLevel) -} - -// Sets the log level to the given level -func setLogLevel(level string) { - switch strings.ToLower(level) { - case "info": - logger.SetLevel(log.InfoLevel) - case "debug": - logger.SetLevel(log.DebugLevel) - case "warn": - logger.SetLevel(log.WarnLevel) - case "error": - logger.SetLevel(log.ErrorLevel) - case "fatal": - logger.SetLevel(log.FatalLevel) - case "panic": - logger.SetLevel(log.PanicLevel) - default: - logger.SetLevel(log.DebugLevel) - logger.Warnf("Log level '%s' not recognised. Setting to Debug.", level) - } -} diff --git a/log_custom.go b/log_custom.go deleted file mode 100644 index b454d2199..000000000 --- a/log_custom.go +++ /dev/null @@ -1,103 +0,0 @@ -package wails - -// CustomLogger is a wrapper object to logrus -type CustomLogger struct { - prefix string - errorOnly bool -} - -func newCustomLogger(prefix string) *CustomLogger { - return &CustomLogger{ - prefix: "[" + prefix + "] ", - } -} - -// Info level message -func (c *CustomLogger) Info(message string) { - logger.Info(c.prefix + message) -} - -// Infof - formatted message -func (c *CustomLogger) Infof(message string, args ...interface{}) { - logger.Infof(c.prefix+message, args...) -} - -// InfoFields - message with fields -func (c *CustomLogger) InfoFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Info(c.prefix + message) -} - -// Debug level message -func (c *CustomLogger) Debug(message string) { - logger.Debug(c.prefix + message) -} - -// Debugf - formatted message -func (c *CustomLogger) Debugf(message string, args ...interface{}) { - logger.Debugf(c.prefix+message, args...) -} - -// DebugFields - message with fields -func (c *CustomLogger) DebugFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Debug(c.prefix + message) -} - -// Warn level message -func (c *CustomLogger) Warn(message string) { - logger.Warn(c.prefix + message) -} - -// Warnf - formatted message -func (c *CustomLogger) Warnf(message string, args ...interface{}) { - logger.Warnf(c.prefix+message, args...) -} - -// WarnFields - message with fields -func (c *CustomLogger) WarnFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Warn(c.prefix + message) -} - -// Error level message -func (c *CustomLogger) Error(message string) { - logger.Error(c.prefix + message) -} - -// Errorf - formatted message -func (c *CustomLogger) Errorf(message string, args ...interface{}) { - logger.Errorf(c.prefix+message, args...) -} - -// ErrorFields - message with fields -func (c *CustomLogger) ErrorFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Error(c.prefix + message) -} - -// Fatal level message -func (c *CustomLogger) Fatal(message string) { - logger.Fatal(c.prefix + message) -} - -// Fatalf - formatted message -func (c *CustomLogger) Fatalf(message string, args ...interface{}) { - logger.Fatalf(c.prefix+message, args...) -} - -// FatalFields - message with fields -func (c *CustomLogger) FatalFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Fatal(c.prefix + message) -} - -// Panic level message -func (c *CustomLogger) Panic(message string) { - logger.Panic(c.prefix + message) -} - -// Panicf - formatted message -func (c *CustomLogger) Panicf(message string, args ...interface{}) { - logger.Panicf(c.prefix+message, args...) -} - -// PanicFields - message with fields -func (c *CustomLogger) PanicFields(message string, fields Fields) { - logger.WithFields(map[string]interface{}(fields)).Panic(c.prefix + message) -} diff --git a/renderer.go b/renderer.go deleted file mode 100644 index f9ba91524..000000000 --- a/renderer.go +++ /dev/null @@ -1,30 +0,0 @@ -package wails - -// Renderer is an interface describing a Wails target to render the app to -type Renderer interface { - Initialise(*AppConfig, *ipcManager, *eventManager) error - Run() error - - // Binding - NewBinding(bindingName string) error - Callback(data string) error - - // Events - NotifyEvent(eventData *eventData) error - - // Injection - AddJSList(js []string) - AddCSSList(css []string) - - // Dialog Runtime - SelectFile() string - SelectDirectory() string - SelectSaveFile() string - - // Window Runtime - SetColour(string) error - Fullscreen() - UnFullscreen() - SetTitle(title string) - Close() -} diff --git a/renderer_headless.go b/renderer_headless.go deleted file mode 100644 index a92b6f4bd..000000000 --- a/renderer_headless.go +++ /dev/null @@ -1,259 +0,0 @@ -package wails - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/dchest/htmlmin" - "github.com/gorilla/websocket" - "github.com/leaanthony/mewn" -) - -type messageType int - -const ( - jsMessage messageType = iota - cssMessage - htmlMessage - notifyMessage - bindingMessage - callbackMessage - wailsRuntimeMessage -) - -func (m messageType) toString() string { - return [...]string{"j", "s", "h", "n", "b", "c", "w"}[m] -} - -// Headless is a backend that opens a local web server -// and renders the files over a websocket -type Headless struct { - // Common - log *CustomLogger - ipcManager *ipcManager - appConfig *AppConfig - eventManager *eventManager - bindingCache []string - frameworkJS string - frameworkCSS string - jsCache []string - cssCache []string - - // Headless specific - initialisationJS []string - server *http.Server - theConnection *websocket.Conn -} - -// Initialise the Headless Renderer -func (h *Headless) Initialise(appConfig *AppConfig, ipcManager *ipcManager, eventManager *eventManager) error { - h.ipcManager = ipcManager - h.appConfig = appConfig - h.eventManager = eventManager - ipcManager.bindRenderer(h) - h.log = newCustomLogger("Headless") - return nil -} - -func (h *Headless) evalJS(js string, mtype messageType) error { - - message := mtype.toString() + js - - if h.theConnection == nil { - h.initialisationJS = append(h.initialisationJS, message) - } else { - // Prepend message type to message - h.sendMessage(h.theConnection, message) - } - - return nil -} - -func (h *Headless) injectCSS(css string) { - // Minify css to overcome issues in the browser with carriage returns - minified, err := htmlmin.Minify([]byte(css), &htmlmin.Options{ - MinifyStyles: true, - }) - if err != nil { - h.log.Fatal("Unable to minify CSS: " + css) - } - minifiedCSS := string(minified) - minifiedCSS = strings.Replace(minifiedCSS, "\\", "\\\\", -1) - minifiedCSS = strings.Replace(minifiedCSS, "'", "\\'", -1) - minifiedCSS = strings.Replace(minifiedCSS, "\n", " ", -1) - inject := fmt.Sprintf("wails._.injectCSS('%s')", minifiedCSS) - h.evalJS(inject, cssMessage) -} - -func (h *Headless) wsBridgeHandler(w http.ResponseWriter, r *http.Request) { - conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024) - if err != nil { - http.Error(w, "Could not open websocket connection", http.StatusBadRequest) - } - h.theConnection = conn - h.log.Infof("Connection from frontend accepted [%p].", h.theConnection) - conn.SetCloseHandler(func(int, string) error { - h.log.Infof("Connection dropped [%p].", h.theConnection) - h.theConnection = nil - return nil - }) - go h.start(conn) -} - -func (h *Headless) sendMessage(conn *websocket.Conn, msg string) { - if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil { - h.log.Error(err.Error()) - } -} - -func (h *Headless) start(conn *websocket.Conn) { - - // set external.invoke - h.log.Infof("Connected to frontend.") - - wailsRuntime := mewn.String("./wailsruntimeassets/default/wails.min.js") - h.evalJS(wailsRuntime, wailsRuntimeMessage) - - // Inject bindings - for _, binding := range h.bindingCache { - h.evalJS(binding, bindingMessage) - } - - // Emit that everything is loaded and ready - h.eventManager.Emit("wails:ready") - - for { - messageType, buffer, err := conn.ReadMessage() - if messageType == -1 { - return - } - if err != nil { - h.log.Errorf("Error reading message: ", err) - continue - } - - h.log.Debugf("Got message: %#v\n", string(buffer)) - - h.ipcManager.Dispatch(string(buffer)) - } -} - -// Run the app in headless mode! -func (h *Headless) Run() error { - h.server = &http.Server{Addr: ":34115"} - http.HandleFunc("/bridge", h.wsBridgeHandler) - - h.log.Info("Headless mode started.") - h.log.Info("The Wails bridge will connect automatically.") - - err := h.server.ListenAndServe() - if err != nil { - h.log.Fatal(err.Error()) - } - return err -} - -// NewBinding creates a new binding with the frontend -func (h *Headless) NewBinding(methodName string) error { - h.bindingCache = append(h.bindingCache, methodName) - return nil -} - -// SelectFile is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) SelectFile() string { - h.log.Error("SelectFile() unsupported in headless mode") - return "" -} - -// SelectDirectory is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) SelectDirectory() string { - h.log.Error("SelectDirectory() unsupported in headless mode") - return "" -} - -// SelectSaveFile is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) SelectSaveFile() string { - h.log.Error("SelectSaveFile() unsupported in headless mode") - return "" -} - -// AddJSList adds a slice of JS strings to the list of js -// files injected at startup -func (h *Headless) AddJSList(jsCache []string) { - h.jsCache = jsCache -} - -// AddCSSList adds a slice of CSS strings to the list of css -// files injected at startup -func (h *Headless) AddCSSList(cssCache []string) { - h.cssCache = cssCache -} - -// Callback sends a callback to the frontend -func (h *Headless) Callback(data string) error { - return h.evalJS(data, callbackMessage) -} - -// NotifyEvent notifies the frontend of an event -func (h *Headless) NotifyEvent(event *eventData) error { - - // Look out! Nils about! - var err error - if event == nil { - err = fmt.Errorf("Sent nil event to renderer.webViewRenderer") - logger.Error(err) - return err - } - - // Default data is a blank array - data := []byte("[]") - - // Process event data - if event.Data != nil { - // Marshall the data - data, err = json.Marshal(event.Data) - if err != nil { - h.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error()) - return err - } - } - - message := fmt.Sprintf("window.wails._.notify('%s','%s')", event.Name, data) - return h.evalJS(message, notifyMessage) -} - -// SetColour is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) SetColour(colour string) error { - h.log.WarnFields("SetColour ignored for headless more", Fields{"col": colour}) - return nil -} - -// Fullscreen is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) Fullscreen() { - h.log.Warn("Fullscreen() unsupported in headless mode") -} - -// UnFullscreen is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) UnFullscreen() { - h.log.Warn("UnFullscreen() unsupported in headless mode") -} - -// SetTitle is currently unsupported for Headless but required -// for the Renderer interface -func (h *Headless) SetTitle(title string) { - h.log.WarnFields("SetTitle() unsupported in headless mode", Fields{"title": title}) -} - -// Close is unsupported for Headless but required -// for the Renderer interface -func (h *Headless) Close() { - h.log.Warn("Close() unsupported in headless mode") -} diff --git a/renderer_webview.go b/renderer_webview.go deleted file mode 100644 index 6defd36ae..000000000 --- a/renderer_webview.go +++ /dev/null @@ -1,367 +0,0 @@ -package wails - -import ( - "encoding/json" - "fmt" - "math/rand" - "sync" - "time" - - "github.com/go-playground/colors" - "github.com/leaanthony/mewn" - "github.com/wailsapp/webview" -) - -// Window defines the main application window -// Default values in [] -type webViewRenderer struct { - window webview.WebView // The webview object - ipc *ipcManager - log *CustomLogger - config *AppConfig - eventManager *eventManager - bindingCache []string - frameworkJS string - frameworkCSS string - - // This is a list of all the JS/CSS that needs injecting - // It will get injected in order - jsCache []string - cssCache []string -} - -// Initialise sets up the WebView -func (w *webViewRenderer) Initialise(config *AppConfig, ipc *ipcManager, eventManager *eventManager) error { - - // Store reference to eventManager - w.eventManager = eventManager - - // Set up logger - w.log = newCustomLogger("WebView") - - // Set up the dispatcher function - w.ipc = ipc - ipc.bindRenderer(w) - - // Save the config - w.config = config - - // Create the WebView instance - w.window = webview.NewWebview(webview.Settings{ - Width: config.Width, - Height: config.Height, - Title: config.Title, - Resizable: config.Resizable, - URL: config.defaultHTML, - Debug: !config.DisableInspector, - ExternalInvokeCallback: func(_ webview.WebView, message string) { - w.ipc.Dispatch(message) - }, - }) - - // SignalManager.OnExit(w.Exit) - - // Set colour - err := w.SetColour(config.Colour) - if err != nil { - return err - } - - w.log.Info("Initialised") - return nil -} - -func (w *webViewRenderer) SetColour(colour string) error { - color, err := colors.Parse(colour) - if err != nil { - return err - } - rgba := color.ToRGBA() - alpha := uint8(255 * rgba.A) - w.window.Dispatch(func() { - w.window.SetColor(rgba.R, rgba.G, rgba.B, alpha) - }) - - return nil -} - -// evalJS evaluates the given js in the WebView -// I should rename this to evilJS lol -func (w *webViewRenderer) evalJS(js string) error { - outputJS := fmt.Sprintf("%.45s", js) - if len(js) > 45 { - outputJS += "..." - } - w.log.DebugFields("Eval", Fields{"js": outputJS}) - // - w.window.Dispatch(func() { - w.window.Eval(js) - }) - return nil -} - -// evalJSSync evaluates the given js in the WebView synchronously -// Do not call this from the main thread or you'll nuke your app because -// you won't get the callback. -func (w *webViewRenderer) evalJSSync(js string) error { - - minified, err := escapeJS(js) - if err != nil { - return err - } - - outputJS := fmt.Sprintf("%.45s", js) - if len(js) > 45 { - outputJS += "..." - } - w.log.DebugFields("EvalSync", Fields{"js": outputJS}) - - ID := fmt.Sprintf("syncjs:%d:%d", time.Now().Unix(), rand.Intn(9999)) - var wg sync.WaitGroup - wg.Add(1) - - go func() { - exit := false - // We are done when we recieve the Callback ID - w.log.Debug("SyncJS: sending with ID = " + ID) - w.eventManager.On(ID, func(...interface{}) { - w.log.Debug("SyncJS: Got callback ID = " + ID) - wg.Done() - exit = true - }) - command := fmt.Sprintf("wails._.addScript('%s', '%s')", minified, ID) - w.window.Dispatch(func() { - w.window.Eval(command) - }) - for exit == false { - time.Sleep(time.Millisecond * 1) - } - }() - - wg.Wait() - - return nil -} - -// injectCSS adds the given CSS to the WebView -func (w *webViewRenderer) injectCSS(css string) { - w.window.Dispatch(func() { - w.window.InjectCSS(css) - }) -} - -// Quit the window -func (w *webViewRenderer) Exit() { - w.window.Exit() -} - -// Run the window main loop -func (w *webViewRenderer) Run() error { - - w.log.Info("Run()") - - // Runtime assets - wailsRuntime := mewn.String("./wailsruntimeassets/default/wails.min.js") - w.evalJS(wailsRuntime) - - // Ping the wait channel when the wails runtime is loaded - w.eventManager.On("wails:loaded", func(...interface{}) { - - // Run this in a different go routine to free up the main process - go func() { - - // Inject Bindings - for _, binding := range w.bindingCache { - w.evalJSSync(binding) - } - - // Inject Framework - if w.frameworkJS != "" { - w.evalJSSync(w.frameworkJS) - } - if w.frameworkCSS != "" { - w.injectCSS(w.frameworkCSS) - } - - // Inject user CSS - if w.config.CSS != "" { - outputCSS := fmt.Sprintf("%.45s", w.config.CSS) - if len(outputCSS) > 45 { - outputCSS += "..." - } - w.log.DebugFields("Inject User CSS", Fields{"css": outputCSS}) - w.injectCSS(w.config.CSS) - } else { - // Use default wails css - w.log.Debug("Injecting Default Wails CSS") - defaultCSS := mewn.String("./wailsruntimeassets/default/wails.css") - - w.injectCSS(defaultCSS) - } - - // Inject all the CSS files that have been added - for _, css := range w.cssCache { - w.injectCSS(css) - } - - // Inject all the JS files that have been added - for _, js := range w.jsCache { - w.evalJSSync(js) - } - - // Inject user JS - if w.config.JS != "" { - outputJS := fmt.Sprintf("%.45s", w.config.JS) - if len(outputJS) > 45 { - outputJS += "..." - } - w.log.DebugFields("Inject User JS", Fields{"js": outputJS}) - w.evalJSSync(w.config.JS) - } - - // Emit that everything is loaded and ready - w.eventManager.Emit("wails:ready") - }() - }) - - // Kick off main window loop - w.window.Run() - - return nil -} - -// Binds the given method name with the front end -func (w *webViewRenderer) NewBinding(methodName string) error { - objectCode := fmt.Sprintf("window.wails._.newBinding('%s');", methodName) - w.bindingCache = append(w.bindingCache, objectCode) - return nil -} - -func (w *webViewRenderer) SelectFile() string { - var result string - - // We need to run this on the main thread, however Dispatch is - // non-blocking so we launch this in a goroutine and wait for - // dispatch to finish before returning the result - var wg sync.WaitGroup - wg.Add(1) - go func() { - w.window.Dispatch(func() { - result = w.window.Dialog(webview.DialogTypeOpen, 0, "Select File", "") - wg.Done() - }) - }() - wg.Wait() - return result -} - -func (w *webViewRenderer) SelectDirectory() string { - var result string - // We need to run this on the main thread, however Dispatch is - // non-blocking so we launch this in a goroutine and wait for - // dispatch to finish before returning the result - var wg sync.WaitGroup - wg.Add(1) - go func() { - w.window.Dispatch(func() { - result = w.window.Dialog(webview.DialogTypeOpen, webview.DialogFlagDirectory, "Select Directory", "") - wg.Done() - }) - }() - wg.Wait() - return result -} - -func (w *webViewRenderer) SelectSaveFile() string { - var result string - // We need to run this on the main thread, however Dispatch is - // non-blocking so we launch this in a goroutine and wait for - // dispatch to finish before returning the result - var wg sync.WaitGroup - wg.Add(1) - go func() { - w.window.Dispatch(func() { - result = w.window.Dialog(webview.DialogTypeSave, 0, "Save file", "") - wg.Done() - }) - }() - wg.Wait() - return result -} - -// AddJS adds a piece of Javascript to a cache that -// gets injected at runtime -func (w *webViewRenderer) AddJSList(jsCache []string) { - w.jsCache = jsCache -} - -// AddCSSList sets the cssCache to the given list of strings -func (w *webViewRenderer) AddCSSList(cssCache []string) { - w.cssCache = cssCache -} - -// Callback sends a callback to the frontend -func (w *webViewRenderer) Callback(data string) error { - callbackCMD := fmt.Sprintf("window.wails._.callback('%s');", data) - return w.evalJS(callbackCMD) -} - -func (w *webViewRenderer) NotifyEvent(event *eventData) error { - - // Look out! Nils about! - var err error - if event == nil { - err = fmt.Errorf("Sent nil event to renderer.webViewRenderer") - logger.Error(err) - return err - } - - // Default data is a blank array - data := []byte("[]") - - // Process event data - if event.Data != nil { - // Marshall the data - data, err = json.Marshal(event.Data) - if err != nil { - w.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error()) - return err - } - } - - message := fmt.Sprintf("wails._.notify('%s','%s')", event.Name, data) - return w.evalJS(message) -} - -// Window -func (w *webViewRenderer) Fullscreen() { - if w.config.Resizable == false { - w.log.Warn("Cannot call Fullscreen() - App.Resizable = false") - return - } - w.window.Dispatch(func() { - w.window.SetFullscreen(true) - }) -} - -func (w *webViewRenderer) UnFullscreen() { - if w.config.Resizable == false { - w.log.Warn("Cannot call UnFullscreen() - App.Resizable = false") - return - } - w.window.Dispatch(func() { - w.window.SetFullscreen(false) - }) -} - -func (w *webViewRenderer) SetTitle(title string) { - w.window.Dispatch(func() { - w.window.SetTitle(title) - }) -} - -func (w *webViewRenderer) Close() { - w.window.Dispatch(func() { - w.window.Terminate() - }) -} diff --git a/runtime.go b/runtime.go deleted file mode 100644 index ff5a522a6..000000000 --- a/runtime.go +++ /dev/null @@ -1,18 +0,0 @@ -package wails - -// Runtime is the Wails Runtime Interface, given to a user who has defined the WailsInit method -type Runtime struct { - Events *RuntimeEvents - Log *RuntimeLog - Dialog *RuntimeDialog - Window *RuntimeWindow -} - -func newRuntime(eventManager *eventManager, renderer Renderer) *Runtime { - return &Runtime{ - Events: newRuntimeEvents(eventManager), - Log: newRuntimeLog(), - Dialog: newRuntimeDialog(renderer), - Window: newRuntimeWindow(renderer), - } -} diff --git a/runtime_dialog.go b/runtime_dialog.go deleted file mode 100644 index f7b4813ed..000000000 --- a/runtime_dialog.go +++ /dev/null @@ -1,28 +0,0 @@ -package wails - -// RuntimeDialog exposes an interface to native dialogs -type RuntimeDialog struct { - renderer Renderer -} - -// newRuntimeDialog creates a new RuntimeDialog struct -func newRuntimeDialog(renderer Renderer) *RuntimeDialog { - return &RuntimeDialog{ - renderer: renderer, - } -} - -// SelectFile prompts the user to select a file -func (r *RuntimeDialog) SelectFile() string { - return r.renderer.SelectFile() -} - -// SelectDirectory prompts the user to select a directory -func (r *RuntimeDialog) SelectDirectory() string { - return r.renderer.SelectDirectory() -} - -// SelectSaveFile prompts the user to select a file for saving -func (r *RuntimeDialog) SelectSaveFile() string { - return r.renderer.SelectSaveFile() -} diff --git a/runtime_events.go b/runtime_events.go deleted file mode 100644 index 331d76a77..000000000 --- a/runtime_events.go +++ /dev/null @@ -1,22 +0,0 @@ -package wails - -// RuntimeEvents exposes the events interface -type RuntimeEvents struct { - eventManager *eventManager -} - -func newRuntimeEvents(eventManager *eventManager) *RuntimeEvents { - return &RuntimeEvents{ - eventManager: eventManager, - } -} - -// On pass through -func (r *RuntimeEvents) On(eventName string, callback func(optionalData ...interface{})) { - r.eventManager.On(eventName, callback) -} - -// Emit pass through -func (r *RuntimeEvents) Emit(eventName string, optionalData ...interface{}) { - r.eventManager.Emit(eventName, optionalData) -} diff --git a/runtime_log.go b/runtime_log.go deleted file mode 100644 index 5ce116992..000000000 --- a/runtime_log.go +++ /dev/null @@ -1,14 +0,0 @@ -package wails - -// RuntimeLog exposes the logging interface to the runtime -type RuntimeLog struct { -} - -func newRuntimeLog() *RuntimeLog { - return &RuntimeLog{} -} - -// New creates a new logger -func (r *RuntimeLog) New(prefix string) *CustomLogger { - return newCustomLogger(prefix) -} diff --git a/runtime_window.go b/runtime_window.go deleted file mode 100644 index a7b4eb5ea..000000000 --- a/runtime_window.go +++ /dev/null @@ -1,37 +0,0 @@ -package wails - -// RuntimeWindow exposes an interface for manipulating the window -type RuntimeWindow struct { - renderer Renderer -} - -func newRuntimeWindow(renderer Renderer) *RuntimeWindow { - return &RuntimeWindow{ - renderer: renderer, - } -} - -// SetColour sets the the window colour -func (r *RuntimeWindow) SetColour(colour string) error { - return r.renderer.SetColour(colour) -} - -// Fullscreen makes the window fullscreen -func (r *RuntimeWindow) Fullscreen() { - r.renderer.Fullscreen() -} - -// UnFullscreen attempts to restore the window to the size/position before fullscreen -func (r *RuntimeWindow) UnFullscreen() { - r.renderer.UnFullscreen() -} - -// SetTitle sets the the window title -func (r *RuntimeWindow) SetTitle(title string) { - r.renderer.SetTitle(title) -} - -// Close shuts down the window and therefore the app -func (r *RuntimeWindow) Close() { - r.renderer.Close() -} diff --git a/scripts/AUTOMATION-README.md b/scripts/AUTOMATION-README.md new file mode 100644 index 000000000..4096b1781 --- /dev/null +++ b/scripts/AUTOMATION-README.md @@ -0,0 +1,123 @@ +# Wails Issue Management Automation + +This directory contains automation workflows and scripts to help manage the Wails project with minimal time investment. + +## GitHub Workflow Files + +### 1. Auto-Label Issues (`auto-label-issues.yml`) +- Automatically labels issues and PRs based on their content and modified files +- Labels are defined in `issue-labeler.yml` and `file-labeler.yml` +- Activates when issues are opened, edited, or reopened + +### 2. Issue Triage Automation (`issue-triage-automation.yml`) +- Performs automated actions for issue triage +- Requests more info for incomplete bug reports +- Prioritizes security issues +- Adds issues to appropriate project boards + +## Configuration Files + +### 1. Issue Content Labeler (`issue-labeler.yml`) +- Defines patterns to match in issue title/body +- Categorizes by version (v2/v3), component, type, and priority +- Customize patterns as needed for your project + +### 2. File Path Labeler (`file-labeler.yml`) +- Labels PRs based on which files they modify +- Helps identify which areas of the codebase are affected +- Customize file patterns as needed + +### 3. Stale Issues Config (`stale.yml`) +- Marks issues as stale after 45 days of inactivity +- Closes stale issues after an additional 10 days +- Exempts issues with important labels + +## Helper Scripts + +### 1. Issue Triage Script (`scripts/issue-triage.ps1`) +- PowerShell script to quickly triage issues +- Lists recent issues needing attention +- Provides easy keyboard shortcuts for common actions +- Run during your dedicated issue triage time + +### 2. PR Review Helper (`scripts/pr-review-helper.ps1`) +- PowerShell script to efficiently review PRs +- Generates review checklists +- Provides easy shortcuts for common review actions +- Run during your dedicated PR review time + +## How to Use This System + +### Daily Workflow (2 hours max) + +**Monday (120 min):** +1. Run `scripts/issue-triage.ps1` (30 min) +2. Run `scripts/pr-review-helper.ps1` (30 min) +3. Check Discord for critical discussions (30 min) +4. Plan your week (30 min) + +**Tuesday-Wednesday (120 min/day):** +1. Quick check for urgent issues (10 min) +2. v3 development (110 min) + +**Thursday (120 min):** +1. v2 maintenance (90 min) +2. Documentation updates (30 min) + +**Friday (120 min):** +1. Run `scripts/pr-review-helper.ps1` (60 min) +2. Discord updates/newsletter (30 min) +3. Weekly reflection (30 min) + +## Installation + +1. The GitHub workflow files should be placed in `.github/workflows/` +2. Configuration files should be placed in `.github/` +3. Helper scripts should be placed in `scripts/` +4. Make sure you have GitHub CLI (`gh`) installed and authenticated + +## Customization + +Feel free to modify the configuration files and scripts to better suit your project's needs: + +1. **Adding New Label Categories**: + - Add new patterns to `issue-labeler.yml` for additional components or types + - Update `file-labeler.yml` if you add new directories or file types + +2. **Adjusting Automation Thresholds**: + - Modify `stale.yml` to change how long issues remain active + - Update `issue-triage-automation.yml` to change conditions for automated actions + +3. **Customizing Scripts**: + - Update the scripts with your specific GitHub username + - Add additional actions based on your workflow preferences + - Adjust time allocations based on which tasks need more attention + +## Benefits + +This automated issue management system will: + +1. **Save Time**: Reduce manual triage of most common issues +2. **Improve Consistency**: Apply the same categorization rules every time +3. **Increase Visibility**: Clear categorization helps community members find issues +4. **Focus Development**: Clearer separation of v2 and v3 work +5. **Reduce Backlog**: Better management of stale issues +6. **Streamline Reviews**: Faster PR processing with guided workflows + +## Requirements + +- GitHub CLI (`gh`) installed and authenticated +- PowerShell 5.1+ for Windows scripts +- GitHub Actions enabled on your repository +- Appropriate permissions to modify workflows + +## Maintenance + +This system requires minimal maintenance: + +- Periodically review and update label patterns as your project evolves +- Adjust time allocations based on where you need to focus +- Update scripts if GitHub CLI commands change +- Customize the workflow as you find pain points in your process + +Remember that the goal is to maximize your limited time (2 hours per day) by automating repetitive tasks and streamlining essential ones. diff --git a/scripts/issue-triage.ps1 b/scripts/issue-triage.ps1 new file mode 100644 index 000000000..6f6edd3ad --- /dev/null +++ b/scripts/issue-triage.ps1 @@ -0,0 +1,108 @@ +# issue-triage.ps1 - Script to help with quick issue triage +# Run this at the start of your GitHub time to quickly process issues + +# Set your GitHub username +$GITHUB_USERNAME = "your-username" + +# Get the latest 10 open issues that aren't assigned and aren't labeled as "awaiting feedback" +Write-Host "Fetching recent unprocessed issues..." +gh issue list --repo wailsapp/wails --limit 10 --json number,title,labels,assignees | Out-File -Encoding utf8 -FilePath "issues_temp.json" +$issues = Get-Content -Raw -Path "issues_temp.json" | ConvertFrom-Json +$newIssues = $issues | Where-Object { + $_.assignees.Count -eq 0 -and + ($_.labels.Count -eq 0 -or -not ($_.labels | Where-Object { $_.name -eq "awaiting feedback" })) +} + +# Process each issue +Write-Host "`n===== Issues Needing Triage =====`n" +foreach ($issue in $newIssues) { + $number = $issue.number + $title = $issue.title + $labelNames = $issue.labels | ForEach-Object { $_.name } + $labelsStr = if ($labelNames) { $labelNames -join ", " } else { "none" } + + Write-Host "Issue #$number`: $title" + Write-Host "Labels: $labelsStr`n" + + $continue = $true + while ($continue) { + Write-Host "Options:" + Write-Host " [v] View issue in browser" + Write-Host " [2] Add v2-only label" + Write-Host " [3] Add v3-alpha label" + Write-Host " [b] Add bug label" + Write-Host " [e] Add enhancement label" + Write-Host " [d] Add documentation label" + Write-Host " [w] Add webview2 label" + Write-Host " [f] Request more info (awaiting feedback)" + Write-Host " [c] Close issue (duplicate/invalid)" + Write-Host " [a] Assign to yourself" + Write-Host " [s] Skip to next issue" + Write-Host " [q] Quit script" + $action = Read-Host "Enter action" + + switch ($action) { + "v" { + gh issue view $number --repo wailsapp/wails --web + } + "2" { + Write-Host "Adding v2-only label..." + gh issue edit $number --repo wailsapp/wails --add-label "v2-only" + } + "3" { + Write-Host "Adding v3-alpha label..." + gh issue edit $number --repo wailsapp/wails --add-label "v3-alpha" + } + "b" { + Write-Host "Adding bug label..." + gh issue edit $number --repo wailsapp/wails --add-label "Bug" + } + "e" { + Write-Host "Adding enhancement label..." + gh issue edit $number --repo wailsapp/wails --add-label "Enhancement" + } + "d" { + Write-Host "Adding documentation label..." + gh issue edit $number --repo wailsapp/wails --add-label "Documentation" + } + "w" { + Write-Host "Adding webview2 label..." + gh issue edit $number --repo wailsapp/wails --add-label "webview2" + } + "f" { + Write-Host "Requesting more info..." + gh issue comment $number --repo wailsapp/wails --body "Thank you for reporting this issue. Could you please provide additional information to help us investigate?`n`n- [Specific details needed]`n`nThis will help us address your issue more effectively." + gh issue edit $number --repo wailsapp/wails --add-label "awaiting feedback" + } + "c" { + $reason = Read-Host "Reason for closing (duplicate/invalid/etc)" + gh issue comment $number --repo wailsapp/wails --body "Closing this issue: $reason" + gh issue close $number --repo wailsapp/wails + } + "a" { + Write-Host "Assigning to yourself..." + gh issue edit $number --repo wailsapp/wails --add-assignee "$GITHUB_USERNAME" + } + "s" { + Write-Host "Skipping to next issue..." + $continue = $false + } + "q" { + Write-Host "Exiting script." + exit + } + default { + Write-Host "Invalid option. Please try again." + } + } + + Write-Host "" + } + + Write-Host "--------------------------------`n" +} + +Write-Host "No more issues to triage!" + +# Clean up temp file +Remove-Item -Path "issues_temp.json" diff --git a/scripts/issue-triage.sh b/scripts/issue-triage.sh new file mode 100644 index 000000000..5809b43a1 --- /dev/null +++ b/scripts/issue-triage.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# issue-triage.sh - Script to help with quick issue triage +# Run this at the start of your GitHub time to quickly process issues + +# Set your GitHub username +GITHUB_USERNAME="your-username" + +# Get the latest 10 open issues that aren't assigned and aren't labeled as "awaiting feedback" +echo "Fetching recent unprocessed issues..." +gh issue list --repo wailsapp/wails --limit 10 --json number,title,labels,assignees --jq '.[] | select(.assignees | length == 0) | select(any(.labels[]; .name != "awaiting feedback"))' > new_issues.json + +# Process each issue +echo -e "\n===== Issues Needing Triage =====\n" +cat new_issues.json | jq -c '.[]' | while read -r issue; do + number=$(echo $issue | jq -r '.number') + title=$(echo $issue | jq -r '.title') + labels=$(echo $issue | jq -r '.labels[] | .name' 2>/dev/null | tr '\n' ', ' | sed 's/,$//') + + if [ -z "$labels" ]; then + labels="none" + fi + + echo -e "Issue #$number: $title" + echo -e "Labels: $labels\n" + + while true; do + echo "Options:" + echo " [v] View issue in browser" + echo " [2] Add v2-only label" + echo " [3] Add v3-alpha label" + echo " [b] Add bug label" + echo " [e] Add enhancement label" + echo " [d] Add documentation label" + echo " [w] Add webview2 label" + echo " [f] Request more info (awaiting feedback)" + echo " [c] Close issue (duplicate/invalid)" + echo " [a] Assign to yourself" + echo " [s] Skip to next issue" + echo " [q] Quit script" + read -p "Enter action: " action + + case $action in + v) + gh issue view $number --repo wailsapp/wails --web + ;; + 2) + echo "Adding v2-only label..." + gh issue edit $number --repo wailsapp/wails --add-label "v2-only" + ;; + 3) + echo "Adding v3-alpha label..." + gh issue edit $number --repo wailsapp/wails --add-label "v3-alpha" + ;; + b) + echo "Adding bug label..." + gh issue edit $number --repo wailsapp/wails --add-label "Bug" + ;; + e) + echo "Adding enhancement label..." + gh issue edit $number --repo wailsapp/wails --add-label "Enhancement" + ;; + d) + echo "Adding documentation label..." + gh issue edit $number --repo wailsapp/wails --add-label "Documentation" + ;; + w) + echo "Adding webview2 label..." + gh issue edit $number --repo wailsapp/wails --add-label "webview2" + ;; + f) + echo "Requesting more info..." + gh issue comment $number --repo wailsapp/wails --body "Thank you for reporting this issue. Could you please provide additional information to help us investigate?\n\n- [Specific details needed]\n\nThis will help us address your issue more effectively." + gh issue edit $number --repo wailsapp/wails --add-label "awaiting feedback" + ;; + c) + read -p "Reason for closing (duplicate/invalid/etc): " reason + gh issue comment $number --repo wailsapp/wails --body "Closing this issue: $reason" + gh issue close $number --repo wailsapp/wails + ;; + a) + echo "Assigning to yourself..." + gh issue edit $number --repo wailsapp/wails --add-assignee "$GITHUB_USERNAME" + ;; + s) + echo "Skipping to next issue..." + break + ;; + q) + echo "Exiting script." + exit 0 + ;; + *) + echo "Invalid option. Please try again." + ;; + esac + + echo "" + done + + echo -e "--------------------------------\n" +done + +echo "No more issues to triage!" diff --git a/scripts/pr-review-helper.ps1 b/scripts/pr-review-helper.ps1 new file mode 100644 index 000000000..75fae4c3b --- /dev/null +++ b/scripts/pr-review-helper.ps1 @@ -0,0 +1,152 @@ +# pr-review-helper.ps1 - Script to help with efficient PR reviews +# Run this during your PR review time + +# Set your GitHub username +$GITHUB_USERNAME = "your-username" + +# Get open PRs that are ready for review +Write-Host "Fetching PRs ready for review..." +gh pr list --repo wailsapp/wails --json number,title,author,labels,reviewDecision,additions,deletions,baseRefName,headRefName --limit 10 | Out-File -Encoding utf8 -FilePath "prs_temp.json" +$prs = Get-Content -Raw -Path "prs_temp.json" | ConvertFrom-Json + +# Process each PR +Write-Host "`n===== PRs Needing Review =====`n" +foreach ($pr in $prs) { + $number = $pr.number + $title = $pr.title + $author = $pr.author.login + $labels = if ($pr.labels) { $pr.labels | ForEach-Object { $_.name } | Join-String -Separator ", " } else { "none" } + $reviewState = if ($pr.reviewDecision) { $pr.reviewDecision } else { "PENDING" } + $baseRef = $pr.baseRefName + $headRef = $pr.headRefName + $changes = $pr.additions + $pr.deletions + + Write-Host "PR #$number`: $title" + Write-Host "Author: $author" + Write-Host "Labels: $labels" + Write-Host "Branch: $headRef -> $baseRef" + Write-Host "Changes: +$($pr.additions)/-$($pr.deletions) lines" + Write-Host "Review state: $reviewState`n" + + # Determine complexity based on size + $complexity = if ($changes -lt 50) { + "Quick review" + } elseif ($changes -lt 300) { + "Moderate review" + } else { + "Extensive review" + } + + Write-Host "Complexity: $complexity" + + $continue = $true + while ($continue) { + Write-Host "`nOptions:" + Write-Host " [v] View PR in browser" + Write-Host " [d] View diff in browser" + Write-Host " [c] Generate review checklist" + Write-Host " [a] Approve PR" + Write-Host " [r] Request changes" + Write-Host " [m] Add comment" + Write-Host " [l] Add labels" + Write-Host " [s] Skip to next PR" + Write-Host " [q] Quit script" + $action = Read-Host "Enter action" + + switch ($action) { + "v" { + gh pr view $number --repo wailsapp/wails --web + } + "d" { + gh pr diff $number --repo wailsapp/wails --web + } + "c" { + # Generate review checklist + $checklist = @" +## PR Review: $title + +### Basic Checks: +- [ ] PR title is descriptive +- [ ] PR description explains the changes +- [ ] Related issues are linked + +### Technical Checks: +- [ ] Code follows project style +- [ ] No unnecessary commented code +- [ ] Error handling is appropriate +- [ ] Documentation updated (if needed) +- [ ] Tests included (if needed) + +### Impact Assessment: +- [ ] Changes are backward compatible (if applicable) +- [ ] No breaking changes to public APIs +- [ ] Performance impact considered + +### Version Specific: +"@ + + if ($baseRef -eq "master") { + $checklist += @" + +- [ ] Appropriate for v2 maintenance +- [ ] No features that should be v3-only +"@ + } elseif ($baseRef -eq "v3-alpha") { + $checklist += @" + +- [ ] Appropriate for v3 development +- [ ] Aligns with v3 roadmap +"@ + } + + # Write to clipboard + $checklist | Set-Clipboard + Write-Host "`nReview checklist copied to clipboard!`n" + } + "a" { + $comment = Read-Host "Approval comment (blank for none)" + if ($comment) { + gh pr review $number --repo wailsapp/wails --approve --body $comment + } else { + gh pr review $number --repo wailsapp/wails --approve + } + } + "r" { + $comment = Read-Host "Feedback for changes requested" + gh pr review $number --repo wailsapp/wails --request-changes --body $comment + } + "m" { + $comment = Read-Host "Comment text" + gh pr comment $number --repo wailsapp/wails --body $comment + } + "l" { + $labels = Read-Host "Labels to add (comma-separated)" + $labelArray = $labels -split "," + foreach ($label in $labelArray) { + $labelTrimmed = $label.Trim() + if ($labelTrimmed) { + gh pr edit $number --repo wailsapp/wails --add-label $labelTrimmed + } + } + } + "s" { + Write-Host "Skipping to next PR..." + $continue = $false + } + "q" { + Write-Host "Exiting script." + exit + } + default { + Write-Host "Invalid option. Please try again." + } + } + } + + Write-Host "--------------------------------`n" +} + +Write-Host "No more PRs to review!" + +# Clean up temp file +Remove-Item -Path "prs_temp.json" diff --git a/scripts/sponsors/generate-sponsor-image.sh b/scripts/sponsors/generate-sponsor-image.sh new file mode 100755 index 000000000..b034a0176 --- /dev/null +++ b/scripts/sponsors/generate-sponsor-image.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +npm install sponsorkit@16.4.2 +npx sponsorkit -o ../../website/static/img/ diff --git a/scripts/sponsors/package-lock.json b/scripts/sponsors/package-lock.json new file mode 100644 index 000000000..2bb15b685 --- /dev/null +++ b/scripts/sponsors/package-lock.json @@ -0,0 +1,723 @@ +{ + "name": "sponsors", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sponsors", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "sponsorkit": "^16.5.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@quansync/fs": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.3.tgz", + "integrity": "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==", + "license": "MIT", + "dependencies": { + "quansync": "^0.2.10" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ansis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sponsorkit": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/sponsorkit/-/sponsorkit-16.5.0.tgz", + "integrity": "sha512-GvlLg88eAEbKzROwAspT+PQTMfHN9KQ+zgPqBBvV1W2jQmKxOtnv9vjgByXvXA2dvTjnksdvbTuwqhJZllyLQA==", + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "cac": "^6.7.14", + "consola": "^3.4.2", + "dotenv": "^16.5.0", + "ofetch": "^1.4.1", + "sharp": "^0.34.2", + "unconfig": "^7.3.2" + }, + "bin": { + "sponsorkit": "bin/sponsorkit.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "license": "MIT" + }, + "node_modules/unconfig": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.3.2.tgz", + "integrity": "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==", + "license": "MIT", + "dependencies": { + "@quansync/fs": "^0.1.1", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "quansync": "^0.2.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + } + } +} diff --git a/scripts/sponsors/package.json b/scripts/sponsors/package.json new file mode 100644 index 000000000..c9f000b90 --- /dev/null +++ b/scripts/sponsors/package.json @@ -0,0 +1,18 @@ +{ + "name": "sponsors", + "version": "1.0.0", + "description": "", + "main": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "sponsorkit": "^16.5.0" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/scripts/sponsors/sponsorkit.config.js b/scripts/sponsors/sponsorkit.config.js new file mode 100644 index 000000000..6596348d1 --- /dev/null +++ b/scripts/sponsors/sponsorkit.config.js @@ -0,0 +1,206 @@ +import {defineConfig} from 'sponsorkit'; + +const helpers = { + avatar: { + size: 45 + }, + boxWidth: 55, + boxHeight: 55, + container: { + sidePadding: 30 + }, +}; + +const coffee = { + avatar: { + size: 50 + }, + boxWidth: 65, + boxHeight: 65, + container: { + sidePadding: 30 + }, +}; + +const breakfast = { + avatar: { + size: 55 + }, + boxWidth: 75, + boxHeight: 75, + container: { + sidePadding: 20 + }, + name: { + maxLength: 10 + } +}; + +const costs = { + avatar: { + size: 65 + }, + boxWidth: 90, + boxHeight: 80, + container: { + sidePadding: 30 + }, + name: { + maxLength: 10 + } +}; + +const bronze = { + avatar: { + size: 85 + }, + boxWidth: 110, + boxHeight: 100, + container: { + sidePadding: 30 + }, + name: { + maxLength: 20 + } +}; + +const silver = { + avatar: { + size: 100 + }, + boxWidth: 110, + boxHeight: 110, + container: { + sidePadding: 20 + }, + name: { + maxLength: 20 + } +}; + +const gold = { + avatar: { + size: 150 + }, + boxWidth: 175, + boxHeight: 175, + container: { + sidePadding: 25 + }, + name: { + maxLength: 25 + } +}; + +const champion = { + avatar: { + size: 175 + }, + boxWidth: 200, + boxHeight: 200, + container: { + sidePadding: 30 + }, + name: { + maxLength: 30 + } +}; + +const partner = { + avatar: { + size: 200 + }, + boxWidth: 225, + boxHeight: 225, + container: { + sidePadding: 40 + }, + name: { + maxLength: 40 + }, + +}; + +export default defineConfig({ + github: { + login: 'leaanthony', + type: 'user', + }, + + // Rendering configs + width: 800, + formats: ['svg'], + tiers: [ + { + title: 'Helpers', + preset: helpers, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Buying Coffee', + monthlyDollars: 5, + preset: coffee, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Buying Breakfast', + monthlyDollars: 10, + preset: breakfast, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Covering Costs', + monthlyDollars: 20, + preset: costs, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Bronze Sponsors', + monthlyDollars: 50, + preset: bronze, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Silver Sponsors', + monthlyDollars: 100, + preset: silver, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Gold Sponsors', + monthlyDollars: 200, + preset: gold, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Champion', + monthlyDollars: 500, + preset: champion, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + { + title: 'Partner', + monthlyDollars: 1000, + preset: partner, + composeAfter: function (composer, tierSponsors, config) { + composer.addSpan(20); + } + }, + ], +}); \ No newline at end of file diff --git a/utils.go b/utils.go deleted file mode 100644 index 4ce362737..000000000 --- a/utils.go +++ /dev/null @@ -1,12 +0,0 @@ -package wails - -import ( - "strings" -) - -func escapeJS(js string) (string, error) { - result := strings.Replace(js, "\\", "\\\\", -1) - result = strings.Replace(result, "'", "\\'", -1) - result = strings.Replace(result, "\n", "\\n", -1) - return result, nil -} diff --git a/v2/.golangci.yml b/v2/.golangci.yml new file mode 100644 index 000000000..66b77ba7f --- /dev/null +++ b/v2/.golangci.yml @@ -0,0 +1,162 @@ +# Options for analysis runner. +run: + # Custom concurrency value + concurrency: 4 + + # Execution timeout + timeout: 10m + + # Exit code when an issue is found. + issues-exit-code: 1 + + # Inclusion of test files + tests: false + + modules-download-mode: readonly + + allow-parallel-runners: false + + go: '1.21' + + +output: + # Runner output format + format: tab + + # Print line of issue code + print-issued-lines: false + + # Append linter to the output + print-linter-name: true + + # Separate issues by line + uniq-by-line: true + + # Output path prefixing + path-prefix: "" + + # Sort results + sort-results: true + + +# Specific linter configs +linters-settings: + errcheck: + check-type-assertions: false + check-blank: false + ignore: fmt:.* + disable-default-exclusions: false + + gofmt: + simplify: true + + gofumpt: + extra-rules: false + +linters: + fast: false + # Enable all available linters. + enable-all: true + # Disable specific linters + disable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - deadcode + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - errchkjson + - errorlint + - execinquery + - exhaustive + - exhaustivestruct + - exhaustruct + - exportloopref + - forbidigo + - forcetypeassert + - funlen + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goerr113 + - goheader + - goimports + - golint + - gomnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - ifshort + - importas + - ineffassign + - interfacebloat + - interfacer + - ireturn + - lll + - loggercheck + - maintidx + - makezero + - maligned + - mirror + - musttag + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - nosnakecase + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - scopelint + - sqlclosecheck + - staticcheck + - structcheck + - stylecheck + - tagalign + - tagliatelle + - tenv + - testableexamples + - testpackage + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - varcheck + - varnamelen + - wastedassign + - whitespace + - wrapcheck + - wsl + - zerologlint \ No newline at end of file diff --git a/v2/.prettierignore b/v2/.prettierignore new file mode 100644 index 000000000..94c6af38e --- /dev/null +++ b/v2/.prettierignore @@ -0,0 +1 @@ +website \ No newline at end of file diff --git a/v2/.prettierrc.yml b/v2/.prettierrc.yml new file mode 100644 index 000000000..685d8b6e7 --- /dev/null +++ b/v2/.prettierrc.yml @@ -0,0 +1,6 @@ +overrides: + - files: + - "**/*.md" + options: + printWidth: 80 + proseWrap: always diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 000000000..c69808f58 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,238 @@ +

+
+

+ +

+ Build desktop applications using Go & Web Technologies. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) + + + +
+ +## Table of Contents + +
+ Click me to Open/Close the directory listing + +- [Table of Contents](#table-of-contents) +- [Introduction](#introduction) + - [Roadmap](#roadmap) +- [Features](#features) +- [Sponsors](#sponsors) +- [Getting Started](#getting-started) +- [FAQ](#faq) +- [Contributors](#contributors) +- [License](#license) + +
+ +## Introduction + +The traditional method of providing web interfaces to Go programs is via a built-in web server. Wails offers a different +approach: it provides the ability to wrap both Go code and a web frontend into a single binary. Tools are provided to +make this easy for you by handling project creation, compilation and bundling. All you have to do is get creative! + +## Features + +- Use standard Go for the backend +- Use any frontend technology you are already familiar with to build your UI +- Quickly create rich frontends for your Go programs using pre-built templates +- Easily call Go methods from Javascript +- Auto-generated Typescript definitions for your Go structs and methods +- Native Dialogs & Menus +- Native Dark / Light mode support +- Supports modern translucency and "frosted window" effects +- Unified eventing system between Go and Javascript +- Powerful cli tool to quickly generate and build your projects +- Multiplatform +- Uses native rendering engines - _no embedded browser_! + +### Roadmap + +The project roadmap may be found [here](https://github.com/wailsapp/wails/discussions/1484). Please consult +this before open up an enhancement request. + +## Sponsors + +This project is supported by these kind people / companies: + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Getting Started + +The installation instructions are on the [official website](https://wails.io/docs/gettingstarted/installation). + +## FAQ + +- Is this an alternative to Electron? + + Depends on your requirements. It's designed to make it easy for Go programmers to make lightweight desktop + applications or add a frontend to their existing applications. Wails does offer native elements such as menus + and dialogs, so it could be considered a lightweight electron alternative. + +- Who is this project aimed at? + + Go programmers who want to bundle an HTML/JS/CSS frontend with their applications, without resorting to creating a + server and opening a browser to view it. + +- What's with the name? + + When I saw WebView, I thought "What I really want is tooling around building a WebView app, a bit like Rails is to + Ruby". So initially it was a play on words (Webview on Rails). It just so happened to also be a homophone of the + English name for the [Country](https://en.wikipedia.org/wiki/Wales) I am from. So it stuck. + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/wailsapp/wails.svg)](https://starchart.cc/wailsapp/wails) + +## Contributors + +The contributors list is getting too big for the readme! All the amazing people who have contributed to this +project have their own page [here](https://wails.io/credits#contributors). + +## License + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +This project was mainly coded to the following albums: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/v2/Taskfile.yaml b/v2/Taskfile.yaml new file mode 100644 index 000000000..d1893732b --- /dev/null +++ b/v2/Taskfile.yaml @@ -0,0 +1,28 @@ +# https://taskfile.dev + +version: "3" + +tasks: + download: + summary: Run go mod tidy + cmds: + - go mod tidy + + lint: + summary: Run golangci-lint + cmds: + - golangci-lint run ./... --timeout=3m -v + + release: + summary: Release a new version of Task. Call with `task v2:release -- ` + dir: tools/release + cmds: + - go run release.go {{.CLI_ARGS}} + + format:md: + cmds: + - npx prettier --write "**/*.md" + + format: + cmds: + - task: format:md diff --git a/v2/cmd/wails/build.go b/v2/cmd/wails/build.go new file mode 100644 index 000000000..39ad00d2f --- /dev/null +++ b/v2/cmd/wails/build.go @@ -0,0 +1,276 @@ +package main + +import ( + "fmt" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" + "os" + "runtime" + "strings" + "time" + + "github.com/leaanthony/slicer" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/build" +) + +func buildApplication(f *flags.Build) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + if quiet { + pterm.DisableOutput() + } else { + app.PrintBanner() + } + + err := f.Process() + if err != nil { + return err + } + + cwd, err := os.Getwd() + if err != nil { + return err + } + projectOptions, err := project.Load(cwd) + if err != nil { + return err + } + + // Set obfuscation from project file + if projectOptions.Obfuscated { + f.Obfuscated = projectOptions.Obfuscated + } + + // Set garble args from project file + if projectOptions.GarbleArgs != "" { + f.GarbleArgs = projectOptions.GarbleArgs + } + + projectTags, err := buildtags.Parse(projectOptions.BuildTags) + if err != nil { + return err + } + userTags := f.GetTags() + compiledTags := append(projectTags, userTags...) + + // Create BuildOptions + buildOptions := &build.Options{ + Logger: logger, + OutputType: "desktop", + OutputFile: f.OutputFilename, + CleanBinDirectory: f.Clean, + Mode: f.GetBuildMode(), + Devtools: f.Debug || f.Devtools, + Pack: !f.NoPackage, + LDFlags: f.LdFlags, + Compiler: f.Compiler, + SkipModTidy: f.SkipModTidy, + Verbosity: f.Verbosity, + ForceBuild: f.ForceBuild, + IgnoreFrontend: f.SkipFrontend, + Compress: f.Upx, + CompressFlags: f.UpxFlags, + UserTags: compiledTags, + WebView2Strategy: f.GetWebView2Strategy(), + TrimPath: f.TrimPath, + RaceDetector: f.RaceDetector, + WindowsConsole: f.WindowsConsole, + Obfuscated: f.Obfuscated, + GarbleArgs: f.GarbleArgs, + SkipBindings: f.SkipBindings, + ProjectData: projectOptions, + SkipEmbedCreate: f.SkipEmbedCreate, + } + + tableData := pterm.TableData{ + {"Platform(s)", f.Platform}, + {"Compiler", f.GetCompilerPath()}, + {"Skip Bindings", bool2Str(f.SkipBindings)}, + {"Build Mode", f.GetBuildModeAsString()}, + {"Devtools", bool2Str(buildOptions.Devtools)}, + {"Frontend Directory", projectOptions.GetFrontendDir()}, + {"Obfuscated", bool2Str(f.Obfuscated)}, + } + if f.Obfuscated { + tableData = append(tableData, []string{"Garble Args", f.GarbleArgs}) + } + tableData = append(tableData, pterm.TableData{ + {"Skip Frontend", bool2Str(f.SkipFrontend)}, + {"Compress", bool2Str(f.Upx)}, + {"Package", bool2Str(!f.NoPackage)}, + {"Clean Bin Dir", bool2Str(f.Clean)}, + {"LDFlags", f.LdFlags}, + {"Tags", "[" + strings.Join(compiledTags, ",") + "]"}, + {"Race Detector", bool2Str(f.RaceDetector)}, + }...) + if len(buildOptions.OutputFile) > 0 && f.GetTargets().Length() == 1 { + tableData = append(tableData, []string{"Output File", f.OutputFilename}) + } + pterm.DefaultSection.Println("Build Options") + + err = pterm.DefaultTable.WithData(tableData).Render() + if err != nil { + return err + } + + if !f.NoSyncGoMod { + err = gomod.SyncGoMod(logger, f.UpdateWailsVersionGoMod) + if err != nil { + return err + } + } + + // Check platform + validPlatformArch := slicer.String([]string{ + "darwin", + "darwin/amd64", + "darwin/arm64", + "darwin/universal", + "linux", + "linux/amd64", + "linux/arm64", + "linux/arm", + "windows", + "windows/amd64", + "windows/arm64", + "windows/386", + }) + + outputBinaries := map[string]string{} + + // Allows cancelling the build after the first error. It would be nice if targets.Each would support funcs + // returning an error. + var targetErr error + targets := f.GetTargets() + targets.Each(func(platform string) { + if targetErr != nil { + return + } + + if !validPlatformArch.Contains(platform) { + buildOptions.Logger.Println("platform '%s' is not supported - skipping. Supported platforms: %s", platform, validPlatformArch.Join(",")) + return + } + + desiredFilename := projectOptions.OutputFilename + if desiredFilename == "" { + desiredFilename = projectOptions.Name + } + desiredFilename = strings.TrimSuffix(desiredFilename, ".exe") + + // Calculate platform and arch + platformSplit := strings.Split(platform, "/") + buildOptions.Platform = platformSplit[0] + buildOptions.Arch = f.GetDefaultArch() + if len(platformSplit) > 1 { + buildOptions.Arch = platformSplit[1] + } + banner := "Building target: " + buildOptions.Platform + "/" + buildOptions.Arch + pterm.DefaultSection.Println(banner) + + if f.Upx && platform == "darwin/universal" { + pterm.Warning.Println("Warning: compress flag unsupported for universal binaries. Ignoring.") + f.Upx = false + } + + switch buildOptions.Platform { + case "linux": + if runtime.GOOS != "linux" { + pterm.Warning.Println("Crosscompiling to Linux not currently supported.") + return + } + case "darwin": + if runtime.GOOS != "darwin" { + pterm.Warning.Println("Crosscompiling to Mac not currently supported.") + return + } + macTargets := targets.Filter(func(platform string) bool { + return strings.HasPrefix(platform, "darwin") + }) + if macTargets.Length() == 2 { + buildOptions.BundleName = fmt.Sprintf("%s-%s.app", desiredFilename, buildOptions.Arch) + } + } + + if targets.Length() > 1 { + // target filename + switch buildOptions.Platform { + case "windows": + desiredFilename = fmt.Sprintf("%s-%s", desiredFilename, buildOptions.Arch) + case "linux", "darwin": + desiredFilename = fmt.Sprintf("%s-%s-%s", desiredFilename, buildOptions.Platform, buildOptions.Arch) + } + } + if buildOptions.Platform == "windows" { + desiredFilename += ".exe" + } + buildOptions.OutputFile = desiredFilename + + if f.OutputFilename != "" { + buildOptions.OutputFile = f.OutputFilename + } + + if f.Obfuscated && f.SkipBindings { + pterm.Warning.Println("obfuscated flag overrides skipbindings flag.") + buildOptions.SkipBindings = false + } + + if !f.DryRun { + // Start Time + start := time.Now() + + compiledBinary, err := build.Build(buildOptions) + if err != nil { + pterm.Error.Println(err.Error()) + targetErr = err + return + } + + buildOptions.IgnoreFrontend = true + buildOptions.CleanBinDirectory = false + + // Output stats + buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.\n", compiledBinary, time.Since(start).Round(time.Millisecond).String())) + + outputBinaries[buildOptions.Platform+"/"+buildOptions.Arch] = compiledBinary + } else { + pterm.Info.Println("Dry run: skipped build.") + } + }) + + if targetErr != nil { + return targetErr + } + + if f.DryRun { + return nil + } + + if f.NSIS { + amd64Binary := outputBinaries["windows/amd64"] + arm64Binary := outputBinaries["windows/arm64"] + if amd64Binary == "" && arm64Binary == "" { + return fmt.Errorf("cannot build nsis installer - no windows targets") + } + + if err := build.GenerateNSISInstaller(buildOptions, amd64Binary, arm64Binary); err != nil { + return err + } + } + + return nil +} diff --git a/v2/cmd/wails/dev.go b/v2/cmd/wails/dev.go new file mode 100644 index 000000000..30213a68e --- /dev/null +++ b/v2/cmd/wails/dev.go @@ -0,0 +1,37 @@ +package main + +import ( + "os" + + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/dev" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +func devApplication(f *flags.Dev) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + if quiet { + pterm.DisableOutput() + } else { + app.PrintBanner() + } + + err := f.Process() + if err != nil { + return err + } + + return dev.Application(f, logger) +} diff --git a/v2/cmd/wails/doctor.go b/v2/cmd/wails/doctor.go new file mode 100644 index 000000000..7f453133d --- /dev/null +++ b/v2/cmd/wails/doctor.go @@ -0,0 +1,262 @@ +package main + +import ( + "fmt" + "runtime" + "runtime/debug" + "strconv" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" + + "github.com/pterm/pterm" + + "github.com/jaypipes/ghw" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/system" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" +) + +func diagnoseEnvironment(f *flags.Doctor) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + pterm.DefaultSection = *pterm.DefaultSection. + WithBottomPadding(0). + WithStyle(pterm.NewStyle(pterm.FgBlue, pterm.Bold)) + + pterm.Println() // Spacer + pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(10).Println("Wails Doctor") + pterm.Println() // Spacer + + spinner, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Scanning system - Please wait (this may take a long time)...") + + // Get system info + info, err := system.GetInfo() + if err != nil { + spinner.Fail() + pterm.Error.Println("Failed to get system information") + return err + } + spinner.Success() + + pterm.DefaultSection.Println("Wails") + + wailsTableData := pterm.TableData{ + {"Version", app.Version()}, + } + + if buildInfo, _ := debug.ReadBuildInfo(); buildInfo != nil { + buildSettingToName := map[string]string{ + "vcs.revision": "Revision", + "vcs.modified": "Modified", + } + for _, buildSetting := range buildInfo.Settings { + name := buildSettingToName[buildSetting.Key] + if name == "" { + continue + } + wailsTableData = append(wailsTableData, []string{name, buildSetting.Value}) + } + } + + // Exit early if PM not found + if info.PM != nil { + wailsTableData = append(wailsTableData, []string{"Package Manager", info.PM.Name()}) + } + + err = pterm.DefaultTable.WithData(wailsTableData).Render() + if err != nil { + return err + } + + pterm.DefaultSection.Println("System") + + systemTabledata := pterm.TableData{ + {pterm.Bold.Sprint("OS"), info.OS.Name}, + {pterm.Bold.Sprint("Version"), info.OS.Version}, + {pterm.Bold.Sprint("ID"), info.OS.ID}, + {pterm.Bold.Sprint("Branding"), info.OS.Branding}, + {pterm.Bold.Sprint("Go Version"), runtime.Version()}, + {pterm.Bold.Sprint("Platform"), runtime.GOOS}, + {pterm.Bold.Sprint("Architecture"), runtime.GOARCH}, + } + + // Probe CPU + cpus, _ := ghw.CPU() + if cpus != nil { + prefix := "CPU" + for idx, cpu := range cpus.Processors { + if len(cpus.Processors) > 1 { + prefix = "CPU " + strconv.Itoa(idx+1) + } + systemTabledata = append(systemTabledata, []string{prefix, cpu.Model}) + } + } else { + cpuInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get CPU info from sysctl + if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "machdep.cpu.brand_string"); err == nil { + cpuInfo = strings.TrimSpace(stdout) + } + } + systemTabledata = append(systemTabledata, []string{"CPU", cpuInfo}) + } + + // Probe GPU + gpu, _ := ghw.GPU(ghw.WithDisableWarnings()) + if gpu != nil { + prefix := "GPU" + for idx, card := range gpu.GraphicsCards { + if len(gpu.GraphicsCards) > 1 { + prefix = "GPU " + strconv.Itoa(idx+1) + " " + } + if card.DeviceInfo == nil { + systemTabledata = append(systemTabledata, []string{prefix, "Unknown"}) + continue + } + details := fmt.Sprintf("%s (%s) - Driver: %s", card.DeviceInfo.Product.Name, card.DeviceInfo.Vendor.Name, card.DeviceInfo.Driver) + systemTabledata = append(systemTabledata, []string{prefix, details}) + } + } else { + gpuInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get GPU info from system_profiler + if stdout, _, err := shell.RunCommand("", "system_profiler", "SPDisplaysDataType"); err == nil { + var ( + startCapturing bool + gpuInfoDetails []string + ) + for _, line := range strings.Split(stdout, "\n") { + if strings.Contains(line, "Chipset Model") { + startCapturing = true + } + if startCapturing { + gpuInfoDetails = append(gpuInfoDetails, strings.TrimSpace(line)) + } + if strings.Contains(line, "Metal Support") { + break + } + } + if len(gpuInfoDetails) > 0 { + gpuInfo = strings.Join(gpuInfoDetails, " ") + } + } + } + systemTabledata = append(systemTabledata, []string{"GPU", gpuInfo}) + } + + memory, _ := ghw.Memory() + if memory != nil { + systemTabledata = append(systemTabledata, []string{"Memory", strconv.Itoa(int(memory.TotalPhysicalBytes/1024/1024/1024)) + "GB"}) + } else { + memInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get Memory info from sysctl + if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "hw.memsize"); err == nil { + if memSize, err := strconv.Atoi(strings.TrimSpace(stdout)); err == nil { + memInfo = strconv.Itoa(memSize/1024/1024/1024) + "GB" + } + } + } + systemTabledata = append(systemTabledata, []string{"Memory", memInfo}) + } + + err = pterm.DefaultTable.WithBoxed().WithData(systemTabledata).Render() + if err != nil { + return err + } + + pterm.DefaultSection.Println("Dependencies") + + // Output Dependencies Status + var dependenciesMissing []string + var externalPackages []*packagemanager.Dependency + dependenciesAvailableRequired := 0 + dependenciesAvailableOptional := 0 + + dependenciesTableData := pterm.TableData{ + {"Dependency", "Package Name", "Status", "Version"}, + } + + hasOptionalDependencies := false + // Loop over dependencies + for _, dependency := range info.Dependencies { + name := dependency.Name + + if dependency.Optional { + name = pterm.Gray("*") + name + hasOptionalDependencies = true + } + + packageName := "Unknown" + status := pterm.LightRed("Not Found") + + // If we found the package + if dependency.PackageName != "" { + packageName = dependency.PackageName + + // If it's installed, update the status + if dependency.Installed { + status = pterm.LightGreen("Installed") + } else { + // Generate meaningful status text + status = pterm.LightMagenta("Available") + + if dependency.Optional { + dependenciesAvailableOptional++ + } else { + dependenciesAvailableRequired++ + } + } + } else { + if !dependency.Optional { + dependenciesMissing = append(dependenciesMissing, dependency.Name) + } + + if dependency.External { + externalPackages = append(externalPackages, dependency) + } + } + + dependenciesTableData = append(dependenciesTableData, []string{name, packageName, status, dependency.Version}) + } + + dependenciesTableString, _ := pterm.DefaultTable.WithHasHeader(true).WithData(dependenciesTableData).Srender() + dependenciesBox := pterm.DefaultBox.WithTitleBottomCenter() + + if hasOptionalDependencies { + dependenciesBox = dependenciesBox.WithTitle(pterm.Gray("*") + " - Optional Dependency") + } + + dependenciesBox.Println(dependenciesTableString) + + pterm.DefaultSection.Println("Diagnosis") + + // Generate an appropriate diagnosis + + if dependenciesAvailableRequired != 0 { + pterm.Println("Required package(s) installation details: \n" + info.Dependencies.InstallAllRequiredCommand()) + } + + if dependenciesAvailableOptional != 0 { + pterm.Println("Optional package(s) installation details: \n" + info.Dependencies.InstallAllOptionalCommand()) + } + + if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 { + pterm.Success.Println("Your system is ready for Wails development!") + } else { + pterm.Warning.Println("Your system has missing dependencies!") + } + + if len(dependenciesMissing) != 0 { + pterm.Println("Fatal:") + pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " ")) + } + + pterm.Println() // Spacer for sponsor message + return nil +} diff --git a/v2/cmd/wails/flags/build.go b/v2/cmd/wails/flags/build.go new file mode 100644 index 000000000..db05c9035 --- /dev/null +++ b/v2/cmd/wails/flags/build.go @@ -0,0 +1,166 @@ +package flags + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/system" + "github.com/wailsapp/wails/v2/pkg/commands/build" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" +) + +const ( + Quiet int = 0 + Normal int = 1 + Verbose int = 2 +) + +// TODO: unify this and `build.Options` +type Build struct { + Common + BuildCommon + + NoPackage bool `description:"Skips platform specific packaging"` + Upx bool `description:"Compress final binary with UPX (if installed)"` + UpxFlags string `description:"Flags to pass to upx"` + Platform string `description:"Platform to target. Comma separate multiple platforms"` + OutputFilename string `name:"o" description:"Output filename"` + Clean bool `description:"Clean the bin directory before building"` + WebView2 string `description:"WebView2 installer strategy: download,embed,browser,error"` + ForceBuild bool `name:"f" description:"Force build of application"` + UpdateWailsVersionGoMod bool `name:"u" description:"Updates go.mod to use the same Wails version as the CLI"` + Debug bool `description:"Builds the application in debug mode"` + Devtools bool `description:"Enable Devtools in productions, Already enabled in debug mode (-debug)"` + NSIS bool `description:"Generate NSIS installer for Windows"` + TrimPath bool `description:"Remove all file system paths from the resulting executable"` + WindowsConsole bool `description:"Keep the console when building for Windows"` + Obfuscated bool `description:"Code obfuscation of bound Wails methods"` + GarbleArgs string `description:"Arguments to pass to garble"` + DryRun bool `description:"Prints the build command without executing it"` + + // Build Specific + + // Internal state + compilerPath string + userTags []string + wv2rtstrategy string // WebView2 runtime strategy + defaultArch string // Default architecture +} + +func (b *Build) Default() *Build { + defaultPlatform := os.Getenv("GOOS") + if defaultPlatform == "" { + defaultPlatform = runtime.GOOS + } + defaultArch := os.Getenv("GOARCH") + if defaultArch == "" { + if system.IsAppleSilicon { + defaultArch = "arm64" + } else { + defaultArch = runtime.GOARCH + } + } + + result := &Build{ + Platform: defaultPlatform + "/" + defaultArch, + WebView2: "download", + GarbleArgs: "-literals -tiny -seed=random", + + defaultArch: defaultArch, + } + result.BuildCommon = result.BuildCommon.Default() + return result +} + +func (b *Build) GetBuildMode() build.Mode { + if b.Debug { + return build.Debug + } + return build.Production +} + +func (b *Build) GetWebView2Strategy() string { + return b.wv2rtstrategy +} + +func (b *Build) GetTargets() *slicer.StringSlicer { + var targets slicer.StringSlicer + targets.AddSlice(strings.Split(b.Platform, ",")) + targets.Deduplicate() + return &targets +} + +func (b *Build) GetCompilerPath() string { + return b.compilerPath +} + +func (b *Build) GetTags() []string { + return b.userTags +} + +func (b *Build) Process() error { + // Lookup compiler path + var err error + b.compilerPath, err = exec.LookPath(b.Compiler) + if err != nil { + return fmt.Errorf("unable to find compiler: %s", b.Compiler) + } + + // Process User Tags + b.userTags, err = buildtags.Parse(b.Tags) + if err != nil { + return err + } + + // WebView2 installer strategy (download by default) + b.WebView2 = strings.ToLower(b.WebView2) + if b.WebView2 != "" { + validWV2Runtime := slicer.String([]string{"download", "embed", "browser", "error"}) + if !validWV2Runtime.Contains(b.WebView2) { + return fmt.Errorf("invalid option for flag 'webview2': %s", b.WebView2) + } + b.wv2rtstrategy = "wv2runtime." + b.WebView2 + } + + return nil +} + +func bool2Str(b bool) string { + if b { + return "true" + } + return "false" +} + +func (b *Build) GetBuildModeAsString() string { + if b.Debug { + return "debug" + } + return "production" +} + +func (b *Build) GetDefaultArch() string { + return b.defaultArch +} + +/* + _, _ = fmt.Fprintf(w, "Frontend Directory: \t%s\n", projectOptions.GetFrontendDir()) + _, _ = fmt.Fprintf(w, "Obfuscated: \t%t\n", buildOptions.Obfuscated) + if buildOptions.Obfuscated { + _, _ = fmt.Fprintf(w, "Garble Args: \t%s\n", buildOptions.GarbleArgs) + } + _, _ = fmt.Fprintf(w, "Skip Frontend: \t%t\n", skipFrontend) + _, _ = fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress) + _, _ = fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack) + _, _ = fmt.Fprintf(w, "Clean Bin Dir: \t%t\n", buildOptions.CleanBinDirectory) + _, _ = fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags) + _, _ = fmt.Fprintf(w, "Tags: \t[%s]\n", strings.Join(buildOptions.UserTags, ",")) + _, _ = fmt.Fprintf(w, "Race Detector: \t%t\n", buildOptions.RaceDetector) + if len(buildOptions.OutputFile) > 0 && targets.Length() == 1 { + _, _ = fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile) + } +*/ diff --git a/v2/cmd/wails/flags/buildcommon.go b/v2/cmd/wails/flags/buildcommon.go new file mode 100644 index 000000000..a22f7a502 --- /dev/null +++ b/v2/cmd/wails/flags/buildcommon.go @@ -0,0 +1,21 @@ +package flags + +type BuildCommon struct { + LdFlags string `description:"Additional ldflags to pass to the compiler"` + Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` + SkipBindings bool `description:"Skips generation of bindings"` + RaceDetector bool `name:"race" description:"Build with Go's race detector"` + SkipFrontend bool `name:"s" description:"Skips building the frontend"` + Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` + Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` + NoSyncGoMod bool `description:"Don't sync go.mod"` + SkipModTidy bool `name:"m" description:"Skip mod tidy before compile"` + SkipEmbedCreate bool `description:"Skips creation of embed files"` +} + +func (c BuildCommon) Default() BuildCommon { + return BuildCommon{ + Compiler: "go", + Verbosity: 1, + } +} diff --git a/v2/cmd/wails/flags/common.go b/v2/cmd/wails/flags/common.go new file mode 100644 index 000000000..e58eff411 --- /dev/null +++ b/v2/cmd/wails/flags/common.go @@ -0,0 +1,5 @@ +package flags + +type Common struct { + NoColour bool `description:"Disable colour in output"` +} diff --git a/v2/cmd/wails/flags/dev.go b/v2/cmd/wails/flags/dev.go new file mode 100644 index 000000000..d31d8bc87 --- /dev/null +++ b/v2/cmd/wails/flags/dev.go @@ -0,0 +1,157 @@ +package flags + +import ( + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "runtime" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/commands/build" +) + +type Dev struct { + BuildCommon + + AssetDir string `flag:"assetdir" description:"Serve assets from the given directory instead of using the provided asset FS"` + Extensions string `flag:"e" description:"Extensions to trigger rebuilds (comma separated) eg go"` + ReloadDirs string `flag:"reloaddirs" description:"Additional directories to trigger reloads (comma separated)"` + Browser bool `flag:"browser" description:"Open the application in a browser"` + NoReload bool `flag:"noreload" description:"Disable reload on asset change"` + NoColour bool `flag:"nocolor" description:"Disable colour in output"` + NoGoRebuild bool `flag:"nogorebuild" description:"Disable automatic rebuilding on backend file changes/additions"` + WailsJSDir string `flag:"wailsjsdir" description:"Directory to generate the Wails JS modules"` + LogLevel string `flag:"loglevel" description:"LogLevel to use - Trace, Debug, Info, Warning, Error)"` + ForceBuild bool `flag:"f" description:"Force build of application"` + Debounce int `flag:"debounce" description:"The amount of time to wait to trigger a reload on change"` + DevServer string `flag:"devserver" description:"The address of the wails dev server"` + AppArgs string `flag:"appargs" description:"arguments to pass to the underlying app (quoted and space separated)"` + Save bool `flag:"save" description:"Save the given flags as defaults"` + FrontendDevServerURL string `flag:"frontenddevserverurl" description:"The url of the external frontend dev server to use"` + ViteServerTimeout int `flag:"viteservertimeout" description:"The timeout in seconds for Vite server detection (default: 10)"` + + // Internal state + devServerURL *url.URL + projectConfig *project.Project +} + +func (*Dev) Default() *Dev { + result := &Dev{ + Extensions: "go", + Debounce: 100, + LogLevel: "Info", + } + result.BuildCommon = result.BuildCommon.Default() + return result +} + +func (d *Dev) Process() error { + var err error + err = d.loadAndMergeProjectConfig() + if err != nil { + return err + } + + if _, _, err := net.SplitHostPort(d.DevServer); err != nil { + return fmt.Errorf("DevServer is not of the form 'host:port', please check your wails.json") + } + + d.devServerURL, err = url.Parse("http://" + d.DevServer) + if err != nil { + return err + } + + return nil +} + +func (d *Dev) loadAndMergeProjectConfig() error { + var err error + cwd, err := os.Getwd() + if err != nil { + return err + } + d.projectConfig, err = project.Load(cwd) + if err != nil { + return err + } + + d.AssetDir, _ = lo.Coalesce(d.AssetDir, d.projectConfig.AssetDirectory) + d.projectConfig.AssetDirectory = filepath.ToSlash(d.AssetDir) + if d.AssetDir != "" { + d.AssetDir, err = filepath.Abs(d.AssetDir) + if err != nil { + return err + } + } + + d.ReloadDirs, _ = lo.Coalesce(d.ReloadDirs, d.projectConfig.ReloadDirectories) + d.projectConfig.ReloadDirectories = filepath.ToSlash(d.ReloadDirs) + d.DevServer, _ = lo.Coalesce(d.DevServer, d.projectConfig.DevServer) + d.projectConfig.DevServer = d.DevServer + d.FrontendDevServerURL, _ = lo.Coalesce(d.FrontendDevServerURL, d.projectConfig.FrontendDevServerURL) + d.projectConfig.FrontendDevServerURL = d.FrontendDevServerURL + d.WailsJSDir, _ = lo.Coalesce(d.WailsJSDir, d.projectConfig.GetWailsJSDir(), d.projectConfig.GetFrontendDir()) + d.projectConfig.WailsJSDir = filepath.ToSlash(d.WailsJSDir) + + if d.Debounce == 100 && d.projectConfig.DebounceMS != 100 { + if d.projectConfig.DebounceMS == 0 { + d.projectConfig.DebounceMS = 100 + } + d.Debounce = d.projectConfig.DebounceMS + } + d.projectConfig.DebounceMS = d.Debounce + + d.AppArgs, _ = lo.Coalesce(d.AppArgs, d.projectConfig.AppArgs) + + if d.ViteServerTimeout == 0 && d.projectConfig.ViteServerTimeout != 0 { + d.ViteServerTimeout = d.projectConfig.ViteServerTimeout + } else if d.ViteServerTimeout == 0 { + d.ViteServerTimeout = 10 // Default timeout + } + d.projectConfig.ViteServerTimeout = d.ViteServerTimeout + + if d.Save { + err = d.projectConfig.Save() + if err != nil { + return err + } + } + + return nil +} + +// GenerateBuildOptions creates a build.Options using the flags +func (d *Dev) GenerateBuildOptions() *build.Options { + result := &build.Options{ + OutputType: "dev", + Mode: build.Dev, + Devtools: true, + Arch: runtime.GOARCH, + Pack: true, + Platform: runtime.GOOS, + LDFlags: d.LdFlags, + Compiler: d.Compiler, + ForceBuild: d.ForceBuild, + IgnoreFrontend: d.SkipFrontend, + SkipBindings: d.SkipBindings, + SkipModTidy: d.SkipModTidy, + Verbosity: d.Verbosity, + WailsJSDir: d.WailsJSDir, + RaceDetector: d.RaceDetector, + ProjectData: d.projectConfig, + SkipEmbedCreate: d.SkipEmbedCreate, + } + + return result +} + +func (d *Dev) ProjectConfig() *project.Project { + return d.projectConfig +} + +func (d *Dev) DevServerURL() *url.URL { + return d.devServerURL +} diff --git a/v2/cmd/wails/flags/doctor.go b/v2/cmd/wails/flags/doctor.go new file mode 100644 index 000000000..e4816b969 --- /dev/null +++ b/v2/cmd/wails/flags/doctor.go @@ -0,0 +1,9 @@ +package flags + +type Doctor struct { + Common +} + +func (b *Doctor) Default() *Doctor { + return &Doctor{} +} diff --git a/v2/cmd/wails/flags/generate.go b/v2/cmd/wails/flags/generate.go new file mode 100644 index 000000000..b14d67017 --- /dev/null +++ b/v2/cmd/wails/flags/generate.go @@ -0,0 +1,21 @@ +package flags + +type GenerateModule struct { + Common + Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` + Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` + Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` +} + +type GenerateTemplate struct { + Common + Name string `description:"Name of the template to generate"` + Frontend string `description:"Frontend to use for the template"` + Quiet bool `description:"Suppress output"` +} + +func (c *GenerateModule) Default() *GenerateModule { + return &GenerateModule{ + Compiler: "go", + } +} diff --git a/v2/cmd/wails/flags/init.go b/v2/cmd/wails/flags/init.go new file mode 100644 index 000000000..16d56a207 --- /dev/null +++ b/v2/cmd/wails/flags/init.go @@ -0,0 +1,21 @@ +package flags + +type Init struct { + Common + + TemplateName string `name:"t" description:"Name of built-in template to use, path to template or template url"` + ProjectName string `name:"n" description:"Name of project"` + CIMode bool `name:"ci" description:"CI Mode"` + ProjectDir string `name:"d" description:"Project directory"` + Quiet bool `name:"q" description:"Suppress output to console"` + InitGit bool `name:"g" description:"Initialise git repository"` + IDE string `name:"ide" description:"Generate IDE project files"` + List bool `name:"l" description:"List templates"` +} + +func (i *Init) Default() *Init { + result := &Init{ + TemplateName: "vanilla", + } + return result +} diff --git a/v2/cmd/wails/flags/show.go b/v2/cmd/wails/flags/show.go new file mode 100644 index 000000000..a8220f3cc --- /dev/null +++ b/v2/cmd/wails/flags/show.go @@ -0,0 +1,6 @@ +package flags + +type ShowReleaseNotes struct { + Common + Version string `description:"The version to show the release notes for"` +} diff --git a/v2/cmd/wails/flags/update.go b/v2/cmd/wails/flags/update.go new file mode 100644 index 000000000..ffd143a9f --- /dev/null +++ b/v2/cmd/wails/flags/update.go @@ -0,0 +1,7 @@ +package flags + +type Update struct { + Common + Version string `description:"The version to update to"` + PreRelease bool `name:"pre" description:"Update to latest pre-release"` +} diff --git a/v2/cmd/wails/generate.go b/v2/cmd/wails/generate.go new file mode 100644 index 000000000..15a6b33d8 --- /dev/null +++ b/v2/cmd/wails/generate.go @@ -0,0 +1,250 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/leaanthony/debme" + "github.com/leaanthony/gosod" + "github.com/pterm/pterm" + "github.com/tidwall/sjson" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/template" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/bindings" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" +) + +func generateModule(f *flags.GenerateModule) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + buildTags, err := buildtags.Parse(f.Tags) + if err != nil { + return err + } + + cwd, err := os.Getwd() + if err != nil { + return err + } + projectConfig, err := project.Load(cwd) + if err != nil { + return err + } + + if projectConfig.Bindings.TsGeneration.OutputType == "" { + projectConfig.Bindings.TsGeneration.OutputType = "classes" + } + + _, err = bindings.GenerateBindings(bindings.Options{ + Compiler: f.Compiler, + Tags: buildTags, + TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, + TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, + TsOutputType: projectConfig.Bindings.TsGeneration.OutputType, + }) + if err != nil { + return err + } + return nil +} + +func generateTemplate(f *flags.GenerateTemplate) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Quiet + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + // name is mandatory + if f.Name == "" { + return fmt.Errorf("please provide a template name using the -name flag") + } + + // If the current directory is not empty, we create a new directory + cwd, err := os.Getwd() + if err != nil { + return err + } + templateDir := filepath.Join(cwd, f.Name) + if !fs.DirExists(templateDir) { + err := os.MkdirAll(templateDir, 0o755) + if err != nil { + return err + } + } + empty, err := fs.DirIsEmpty(templateDir) + if err != nil { + return err + } + + pterm.DefaultSection.Println("Generating template") + + if !empty { + templateDir = filepath.Join(cwd, f.Name) + printBulletPoint("Creating new template directory:", f.Name) + err = fs.Mkdir(templateDir) + if err != nil { + return err + } + } + + // Create base template + baseTemplate, err := debme.FS(template.Base, "base") + if err != nil { + return err + } + g := gosod.New(baseTemplate) + g.SetTemplateFilters([]string{".template"}) + + err = os.Chdir(templateDir) + if err != nil { + return err + } + + type templateData struct { + Name string + Description string + TemplateDir string + WailsVersion string + } + + printBulletPoint("Extracting base template files...") + + err = g.Extract(templateDir, &templateData{ + Name: f.Name, + TemplateDir: templateDir, + WailsVersion: app.Version(), + }) + if err != nil { + return err + } + + err = os.Chdir(cwd) + if err != nil { + return err + } + + // If we aren't migrating the files, just exit + if f.Frontend == "" { + pterm.Println() + pterm.Println() + pterm.Info.Println("No frontend specified to migrate. Template created.") + pterm.Println() + return nil + } + + // Remove frontend directory + frontendDir := filepath.Join(templateDir, "frontend") + err = os.RemoveAll(frontendDir) + if err != nil { + return err + } + + // Copy the files into a new frontend directory + printBulletPoint("Migrating existing project files to frontend directory...") + + sourceDir, err := filepath.Abs(f.Frontend) + if err != nil { + return err + } + + newFrontendDir := filepath.Join(templateDir, "frontend") + err = fs.CopyDirExtended(sourceDir, newFrontendDir, []string{f.Name, "node_modules"}) + if err != nil { + return err + } + + // Process package.json + err = processPackageJSON(frontendDir) + if err != nil { + return err + } + + // Process package-lock.json + err = processPackageLockJSON(frontendDir) + if err != nil { + return err + } + + // Remove node_modules - ignore error, eg it doesn't exist + _ = os.RemoveAll(filepath.Join(frontendDir, "node_modules")) + + return nil +} + +func processPackageJSON(frontendDir string) error { + var err error + + packageJSON := filepath.Join(frontendDir, "package.json") + if !fs.FileExists(packageJSON) { + return fmt.Errorf("no package.json found - cannot process") + } + + json, err := os.ReadFile(packageJSON) + if err != nil { + return err + } + + // We will ignore these errors - it's not critical + printBulletPoint("Updating package.json data...") + json, _ = sjson.SetBytes(json, "name", "{{.ProjectName}}") + json, _ = sjson.SetBytes(json, "author", "{{.AuthorName}}") + + err = os.WriteFile(packageJSON, json, 0o644) + if err != nil { + return err + } + baseDir := filepath.Dir(packageJSON) + printBulletPoint("Renaming package.json -> package.tmpl.json...") + err = os.Rename(packageJSON, filepath.Join(baseDir, "package.tmpl.json")) + if err != nil { + return err + } + return nil +} + +func processPackageLockJSON(frontendDir string) error { + var err error + + filename := filepath.Join(frontendDir, "package-lock.json") + if !fs.FileExists(filename) { + return fmt.Errorf("no package-lock.json found - cannot process") + } + + data, err := os.ReadFile(filename) + if err != nil { + return err + } + json := string(data) + + // We will ignore these errors - it's not critical + printBulletPoint("Updating package-lock.json data...") + json, _ = sjson.Set(json, "name", "{{.ProjectName}}") + + err = os.WriteFile(filename, []byte(json), 0o644) + if err != nil { + return err + } + baseDir := filepath.Dir(filename) + printBulletPoint("Renaming package-lock.json -> package-lock.tmpl.json...") + err = os.Rename(filename, filepath.Join(baseDir, "package-lock.tmpl.json")) + if err != nil { + return err + } + return nil +} diff --git a/v2/cmd/wails/init.go b/v2/cmd/wails/init.go new file mode 100644 index 000000000..f79e37ffc --- /dev/null +++ b/v2/cmd/wails/init.go @@ -0,0 +1,295 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/flytam/filenamify" + "github.com/leaanthony/slicer" + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/pkg/buildassets" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/git" + "github.com/wailsapp/wails/v2/pkg/templates" +) + +func initProject(f *flags.Init) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + // Are we listing templates? + if f.List { + app.PrintBanner() + templateList, err := templates.List() + if err != nil { + return err + } + + pterm.DefaultSection.Println("Available templates") + + table := pterm.TableData{{"Template", "Short Name", "Description"}} + for _, template := range templateList { + table = append(table, []string{template.Name, template.ShortName, template.Description}) + } + err = pterm.DefaultTable.WithHasHeader(true).WithBoxed(true).WithData(table).Render() + pterm.Println() + return err + } + + // Validate name + if len(f.ProjectName) == 0 { + return fmt.Errorf("please provide a project name using the -n flag") + } + + // Validate IDE option + supportedIDEs := slicer.String([]string{"vscode", "goland"}) + ide := strings.ToLower(f.IDE) + if ide != "" { + if !supportedIDEs.Contains(ide) { + return fmt.Errorf("ide '%s' not supported. Valid values: %s", ide, supportedIDEs.Join(" ")) + } + } + + if !quiet { + app.PrintBanner() + } + + pterm.DefaultSection.Printf("Initialising Project '%s'", f.ProjectName) + + projectFilename, err := filenamify.Filenamify(f.ProjectName, filenamify.Options{ + Replacement: "_", + MaxLength: 255, + }) + if err != nil { + return err + } + goBinary, err := exec.LookPath("go") + if err != nil { + return fmt.Errorf("unable to find Go compiler. Please download and install Go: https://golang.org/dl/") + } + + // Get base path and convert to forward slashes + goPath := filepath.ToSlash(filepath.Dir(goBinary)) + // Trim bin directory + goSDKPath := strings.TrimSuffix(goPath, "/bin") + + // Create Template Options + options := &templates.Options{ + ProjectName: f.ProjectName, + TargetDir: f.ProjectDir, + TemplateName: f.TemplateName, + Logger: logger, + IDE: ide, + InitGit: f.InitGit, + ProjectNameFilename: projectFilename, + WailsVersion: app.Version(), + GoSDKPath: goSDKPath, + } + + // Try to discover author details from git config + findAuthorDetails(options) + + // Start Time + start := time.Now() + + // Install the template + remote, template, err := templates.Install(options) + if err != nil { + return err + } + + // Install the default assets + err = buildassets.Install(options.TargetDir) + if err != nil { + return err + } + + err = os.Chdir(options.TargetDir) + if err != nil { + return err + } + + // Change the module name to project name + err = updateModuleNameToProjectName(options, quiet) + if err != nil { + return err + } + + if !f.CIMode { + // Run `go mod tidy` to ensure `go.sum` is up to date + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = options.TargetDir + cmd.Stderr = os.Stderr + if !quiet { + cmd.Stdout = os.Stdout + } + err = cmd.Run() + if err != nil { + return err + } + } else { + // Update go mod + workspace := os.Getenv("GITHUB_WORKSPACE") + pterm.Println("GitHub workspace:", workspace) + if workspace == "" { + os.Exit(1) + } + updateReplaceLine(workspace) + } + + // Remove the `.git`` directory in the template project + err = os.RemoveAll(".git") + if err != nil { + return err + } + + if options.InitGit { + err = initGit(options) + if err != nil { + return err + } + } + + if quiet { + return nil + } + + // Output stats + elapsed := time.Since(start) + + // Create pterm table + table := pterm.TableData{ + {"Project Name", options.ProjectName}, + {"Project Directory", options.TargetDir}, + {"Template", template.Name}, + {"Template Source", template.HelpURL}, + } + err = pterm.DefaultTable.WithData(table).Render() + if err != nil { + return err + } + + // IDE message + switch options.IDE { + case "vscode": + pterm.Println() + pterm.Info.Println("VSCode config files generated.") + case "goland": + pterm.Println() + pterm.Info.Println("Goland config files generated.") + } + + if options.InitGit { + pterm.Info.Println("Git repository initialised.") + } + + if remote { + pterm.Warning.Println("NOTE: You have created a project using a remote template. The Wails project takes no responsibility for 3rd party templates. Only use remote templates that you trust.") + } + + pterm.Println("") + pterm.Printf("Initialised project '%s' in %s.\n", options.ProjectName, elapsed.Round(time.Millisecond).String()) + pterm.Println("") + + return nil +} + +func initGit(options *templates.Options) error { + err := git.InitRepo(options.TargetDir) + if err != nil { + return errors.Wrap(err, "Unable to initialise git repository:") + } + + ignore := []string{ + "build/bin", + "frontend/dist", + "frontend/node_modules", + } + err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0o644) + if err != nil { + return errors.Wrap(err, "Unable to create gitignore") + } + + return nil +} + +// findAuthorDetails tries to find the user's name and email +// from gitconfig. If it finds them, it stores them in the project options +func findAuthorDetails(options *templates.Options) { + if git.IsInstalled() { + name, err := git.Name() + if err == nil { + options.AuthorName = strings.TrimSpace(name) + } + + email, err := git.Email() + if err == nil { + options.AuthorEmail = strings.TrimSpace(email) + } + } +} + +func updateReplaceLine(targetPath string) { + file, err := os.Open("go.mod") + if err != nil { + fatal(err.Error()) + } + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + err = file.Close() + if err != nil { + fatal(err.Error()) + } + + if err := scanner.Err(); err != nil { + fatal(err.Error()) + } + + for i, line := range lines { + println(line) + if strings.HasPrefix(line, "// replace") { + pterm.Println("Found replace line") + splitLine := strings.Split(line, " ") + splitLine[5] = targetPath + "/v2" + lines[i] = strings.Join(splitLine[1:], " ") + continue + } + } + + err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0o644) + if err != nil { + fatal(err.Error()) + } +} + +func updateModuleNameToProjectName(options *templates.Options, quiet bool) error { + cmd := exec.Command("go", "mod", "edit", "-module", options.ProjectName) + cmd.Dir = options.TargetDir + cmd.Stderr = os.Stderr + if !quiet { + cmd.Stdout = os.Stdout + } + + return cmd.Run() +} diff --git a/v2/cmd/wails/internal/dev/dev.go b/v2/cmd/wails/internal/dev/dev.go new file mode 100644 index 000000000..9495b5bf2 --- /dev/null +++ b/v2/cmd/wails/internal/dev/dev.go @@ -0,0 +1,525 @@ +package dev + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod" + "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "golang.org/x/mod/semver" + + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" + + "github.com/google/shlex" + + "github.com/pkg/browser" + + "github.com/fsnotify/fsnotify" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/process" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/build" +) + +const ( + viteMinVersion = "v3.0.0" +) + +func sliceToMap(input []string) map[string]struct{} { + result := map[string]struct{}{} + for _, value := range input { + result[value] = struct{}{} + } + return result +} + +// Application runs the application in dev mode +func Application(f *flags.Dev, logger *clilogger.CLILogger) error { + cwd := lo.Must(os.Getwd()) + + // Update go.mod to use current wails version + err := gomod.SyncGoMod(logger, !f.NoSyncGoMod) + if err != nil { + return err + } + + if !f.SkipModTidy { + // Run go mod tidy to ensure we're up-to-date + err = runCommand(cwd, false, f.Compiler, "mod", "tidy") + if err != nil { + return err + } + } + + buildOptions := f.GenerateBuildOptions() + buildOptions.Logger = logger + + userTags, err := buildtags.Parse(f.Tags) + if err != nil { + return err + } + + projectConfig := f.ProjectConfig() + + projectTags, err := buildtags.Parse(projectConfig.BuildTags) + if err != nil { + return err + } + compiledTags := append(projectTags, userTags...) + buildOptions.UserTags = compiledTags + + // Setup signal handler + quitChannel := make(chan os.Signal, 1) + signal.Notify(quitChannel, os.Interrupt, syscall.SIGTERM) + exitCodeChannel := make(chan int, 1) + + // Build the frontend if requested, but ignore building the application itself. + ignoreFrontend := buildOptions.IgnoreFrontend + if !ignoreFrontend { + buildOptions.IgnoreApplication = true + if _, err := build.Build(buildOptions); err != nil { + return err + } + buildOptions.IgnoreApplication = false + } + + legacyUseDevServerInsteadofCustomScheme := false + // frontend:dev:watcher command. + frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery() + if command := projectConfig.DevWatcherCommand; command != "" { + closer, devServerURL, devServerViteVersion, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery, projectConfig.ViteServerTimeout) + if err != nil { + return err + } + if devServerURL != "" { + projectConfig.FrontendDevServerURL = devServerURL + f.FrontendDevServerURL = devServerURL + } + defer closer() + + if devServerViteVersion != "" && semver.Compare(devServerViteVersion, viteMinVersion) < 0 { + logutils.LogRed("Please upgrade your Vite Server to at least '%s' future Wails versions will require at least Vite '%s'", viteMinVersion, viteMinVersion) + time.Sleep(3 * time.Second) + legacyUseDevServerInsteadofCustomScheme = true + } + } else if frontendDevAutoDiscovery { + return fmt.Errorf("unable to auto discover frontend:dev:serverUrl without a frontend:dev:watcher command, please either set frontend:dev:watcher or remove the auto discovery from frontend:dev:serverUrl") + } + + // Do initial build but only for the application. + logger.Println("Building application for development...") + buildOptions.IgnoreFrontend = true + debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme) + buildOptions.IgnoreFrontend = ignoreFrontend || f.FrontendDevServerURL != "" + if err != nil { + return err + } + defer func() { + if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { + logutils.LogDarkYellow("Unable to kill process and cleanup binary: %s", err) + } + }() + + // open browser + if f.Browser { + err = browser.OpenURL(f.DevServerURL().String()) + if err != nil { + return err + } + } + + logutils.LogGreen("Using DevServer URL: %s", f.DevServerURL()) + if f.FrontendDevServerURL != "" { + logutils.LogGreen("Using Frontend DevServer URL: %s", f.FrontendDevServerURL) + } + logutils.LogGreen("Using reload debounce setting of %d milliseconds", f.Debounce) + + // Show dev server URL in terminal after 3 seconds + go func() { + time.Sleep(3 * time.Second) + logutils.LogGreen("\n\nTo develop in the browser and call your bound Go methods from Javascript, navigate to: %s", f.DevServerURL()) + }() + + // Watch for changes and trigger restartApp() + debugBinaryProcess, err = doWatcherLoop(cwd, projectConfig.ReloadDirectories, buildOptions, debugBinaryProcess, f, exitCodeChannel, quitChannel, f.DevServerURL(), legacyUseDevServerInsteadofCustomScheme) + if err != nil { + return err + } + + // Kill the current program if running and remove dev binary + if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { + return err + } + + // Reset the process and the binary so defer knows about it and is a nop. + debugBinaryProcess = nil + appBinary = "" + + logutils.LogGreen("Development mode exited") + + return nil +} + +func killProcessAndCleanupBinary(process *process.Process, binary string) error { + if process != nil && process.Running { + if err := process.Kill(); err != nil { + return err + } + } + + if binary != "" { + err := os.Remove(binary) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + return nil +} + +func runCommand(dir string, exitOnError bool, command string, args ...string) error { + logutils.LogGreen("Executing: " + command + " " + strings.Join(args, " ")) + cmd := exec.Command(command, args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + println(string(output)) + println(err.Error()) + if exitOnError { + os.Exit(1) + } + return err + } + return nil +} + +// runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev` +func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool, viteServerTimeout int) (func(), string, string, error) { + ctx, cancel := context.WithCancel(context.Background()) + scanner := NewStdoutScanner() + cmdSlice := strings.Split(devCommand, " ") + cmd := exec.CommandContext(ctx, cmdSlice[0], cmdSlice[1:]...) + cmd.Stderr = os.Stderr + cmd.Stdout = scanner + cmd.Dir = frontendDirectory + setParentGID(cmd) + + if err := cmd.Start(); err != nil { + cancel() + return nil, "", "", fmt.Errorf("unable to start frontend DevWatcher: %w", err) + } + + var viteServerURL string + if discoverViteServerURL { + select { + case serverURL := <-scanner.ViteServerURLChan: + viteServerURL = serverURL + case <-time.After(time.Second * time.Duration(viteServerTimeout)): + cancel() + return nil, "", "", fmt.Errorf("failed to find Vite server URL: Timed out waiting for Vite to output a URL after %d seconds", viteServerTimeout) + } + } + + viteVersion := "" + select { + case version := <-scanner.ViteServerVersionC: + viteVersion = version + + case <-time.After(time.Second * 5): + // That's fine, then most probably it was not vite that was running + } + + logutils.LogGreen("Running frontend DevWatcher command: '%s'", devCommand) + var wg sync.WaitGroup + wg.Add(1) + + const ( + stateRunning int32 = 0 + stateCanceling int32 = 1 + stateStopped int32 = 2 + ) + state := stateRunning + go func() { + if err := cmd.Wait(); err != nil { + wasRunning := atomic.CompareAndSwapInt32(&state, stateRunning, stateStopped) + if err.Error() != "exit status 1" && wasRunning { + logutils.LogRed("Error from DevWatcher '%s': %s", devCommand, err.Error()) + } + } + atomic.StoreInt32(&state, stateStopped) + wg.Done() + }() + + return func() { + if atomic.CompareAndSwapInt32(&state, stateRunning, stateCanceling) { + killProc(cmd, devCommand) + } + cancel() + wg.Wait() + }, viteServerURL, viteVersion, nil +} + +// restartApp does the actual rebuilding of the application when files change +func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, string, error) { + appBinary, err := build.Build(buildOptions) + println() + if err != nil { + logutils.LogRed("Build error - " + err.Error()) + + msg := "Continuing to run current version" + if debugBinaryProcess == nil { + msg = "No version running, build will be retriggered as soon as changes have been detected" + } + logutils.LogDarkYellow(msg) + return nil, "", nil + } + + // Kill existing binary if need be + if debugBinaryProcess != nil { + killError := debugBinaryProcess.Kill() + + if killError != nil { + buildOptions.Logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID()) + } + + debugBinaryProcess = nil + } + + // parse appargs if any + args, err := shlex.Split(f.AppArgs) + if err != nil { + buildOptions.Logger.Fatal("Unable to parse appargs: %s", err.Error()) + } + + // Set environment variables accordingly + os.Setenv("loglevel", f.LogLevel) + os.Setenv("assetdir", f.AssetDir) + os.Setenv("devserver", f.DevServer) + os.Setenv("frontenddevserverurl", f.FrontendDevServerURL) + + // Start up new binary with correct args + newProcess := process.NewProcess(appBinary, args...) + err = newProcess.Start(exitCodeChannel) + if err != nil { + // Remove binary + if fs.FileExists(appBinary) { + deleteError := fs.DeleteFile(appBinary) + if deleteError != nil { + buildOptions.Logger.Fatal("Unable to delete app binary: " + appBinary) + } + } + buildOptions.Logger.Fatal("Unable to start application: %s", err.Error()) + } + + return newProcess, appBinary, nil +} + +// doWatcherLoop is the main watch loop that runs while dev is active +func doWatcherLoop(cwd string, reloadDirs string, buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, error) { + // create the project files watcher + watcher, err := initialiseWatcher(cwd, reloadDirs) + if err != nil { + logutils.LogRed("Unable to create filesystem watcher. Reloads will not occur.") + return nil, err + } + + defer func(watcher *fsnotify.Watcher) { + err := watcher.Close() + if err != nil { + log.Fatal(err.Error()) + } + }(watcher) + + logutils.LogGreen("Watching (sub)/directory: %s", cwd) + + // Main Loop + extensionsThatTriggerARebuild := sliceToMap(strings.Split(f.Extensions, ",")) + var dirsThatTriggerAReload []string + for _, dir := range strings.Split(f.ReloadDirs, ",") { + if dir == "" { + continue + } + thePath, err := filepath.Abs(dir) + if err != nil { + logutils.LogRed("Unable to expand reloadDir '%s': %s", dir, err) + continue + } + dirsThatTriggerAReload = append(dirsThatTriggerAReload, thePath) + err = watcher.Add(thePath) + if err != nil { + logutils.LogRed("Unable to watch path: %s due to error %v", thePath, err) + } else { + logutils.LogGreen("Watching (sub)/directory: %s", thePath) + } + } + + quit := false + interval := time.Duration(f.Debounce) * time.Millisecond + timer := time.NewTimer(interval) + rebuild := false + reload := false + assetDir := "" + changedPaths := map[string]struct{}{} + + // If we are using an external dev server, the reloading of the frontend part can be skipped or if the user requested it + skipAssetsReload := f.FrontendDevServerURL != "" || f.NoReload + + assetDirURL := joinPath(devServerURL, "/wails/assetdir") + reloadURL := joinPath(devServerURL, "/wails/reload") + for !quit { + // reload := false + select { + case exitCode := <-exitCodeChannel: + if exitCode == 0 { + quit = true + } + case err := <-watcher.Errors: + logutils.LogDarkYellow(err.Error()) + case item := <-watcher.Events: + isEligibleFile := func(fileName string) bool { + // Iterate all file patterns + ext := filepath.Ext(fileName) + if ext != "" { + ext = ext[1:] + if _, exists := extensionsThatTriggerARebuild[ext]; exists { + return true + } + } + return false + } + + // Handle write operations + if item.Op&fsnotify.Write == fsnotify.Write { + // Ignore directories + itemName := item.Name + if fs.DirExists(itemName) { + continue + } + + if isEligibleFile(itemName) { + rebuild = true + timer.Reset(interval) + continue + } + + for _, reloadDir := range dirsThatTriggerAReload { + if strings.HasPrefix(itemName, reloadDir) { + reload = true + break + } + } + + if !reload { + changedPaths[filepath.Dir(itemName)] = struct{}{} + } + + timer.Reset(interval) + } + + // Handle new fs entries that are created + if item.Op&fsnotify.Create == fsnotify.Create { + // If this is a folder, add it to our watch list + if fs.DirExists(item.Name) { + // node_modules is BANNED! + if !strings.Contains(item.Name, "node_modules") { + err := watcher.Add(item.Name) + if err != nil { + buildOptions.Logger.Fatal("%s", err.Error()) + } + logutils.LogGreen("Added new directory to watcher: %s", item.Name) + } + } else if isEligibleFile(item.Name) { + // Handle creation of new file. + // Note: On some platforms an update to a file is represented as + // REMOVE -> CREATE instead of WRITE, so this is not only new files + // but also updates to existing files + rebuild = true + timer.Reset(interval) + continue + } + } + case <-timer.C: + if rebuild { + rebuild = false + if f.NoGoRebuild { + logutils.LogGreen("[Rebuild triggered] skipping due to flag -nogorebuild") + } else { + logutils.LogGreen("[Rebuild triggered] files updated") + // Try and build the app + + newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme) + if err != nil { + logutils.LogRed("Error during build: %s", err.Error()) + continue + } + // If we have a new process, saveConfig it + if newBinaryProcess != nil { + debugBinaryProcess = newBinaryProcess + } + } + } + + if !skipAssetsReload && len(changedPaths) != 0 { + if assetDir == "" { + resp, err := http.Get(assetDirURL) + if err != nil { + logutils.LogRed("Error during retrieving assetdir: %s", err.Error()) + } else { + content, err := io.ReadAll(resp.Body) + if err != nil { + logutils.LogRed("Error reading assetdir from devserver: %s", err.Error()) + } else { + assetDir = string(content) + } + resp.Body.Close() + } + } + + if assetDir != "" { + for thePath := range changedPaths { + if strings.HasPrefix(thePath, assetDir) { + reload = true + break + } + } + } else if len(dirsThatTriggerAReload) == 0 { + logutils.LogRed("Reloading couldn't be triggered: Please specify -assetdir or -reloaddirs") + } + } + if reload { + reload = false + _, err := http.Get(reloadURL) + if err != nil { + logutils.LogRed("Error during refresh: %s", err.Error()) + } + } + changedPaths = map[string]struct{}{} + case <-quitChannel: + logutils.LogGreen("\nCaught quit") + quit = true + } + } + return debugBinaryProcess, nil +} + +func joinPath(url *url.URL, subPath string) string { + u := *url + u.Path = path.Join(u.Path, subPath) + return u.String() +} diff --git a/v2/cmd/wails/internal/dev/dev_other.go b/v2/cmd/wails/internal/dev/dev_other.go new file mode 100644 index 000000000..88e170ee3 --- /dev/null +++ b/v2/cmd/wails/internal/dev/dev_other.go @@ -0,0 +1,37 @@ +//go:build darwin || linux +// +build darwin linux + +package dev + +import ( + "os/exec" + "syscall" + + "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "golang.org/x/sys/unix" +) + +func setParentGID(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} + +func killProc(cmd *exec.Cmd, devCommand string) { + if cmd == nil || cmd.Process == nil { + return + } + + // Experiencing the same issue on macOS BigSur + // I'm using Vite, but I would imagine this could be an issue with Node (npm) in general + // Also, after several edit/rebuild cycles any abnormal shutdown (crash or CTRL-C) may still leave Node running + // Credit: https://stackoverflow.com/a/29552044/14764450 (same page as the Windows solution above) + // Not tested on *nix + pgid, err := syscall.Getpgid(cmd.Process.Pid) + if err == nil { + err := syscall.Kill(-pgid, unix.SIGTERM) // note the minus sign + if err != nil { + logutils.LogRed("Error from '%s' when attempting to kill the process: %s", devCommand, err.Error()) + } + } +} diff --git a/v2/cmd/wails/internal/dev/dev_windows.go b/v2/cmd/wails/internal/dev/dev_windows.go new file mode 100644 index 000000000..e219e6519 --- /dev/null +++ b/v2/cmd/wails/internal/dev/dev_windows.go @@ -0,0 +1,34 @@ +//go:build windows +// +build windows + +package dev + +import ( + "bytes" + "os/exec" + "strconv" + + "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" +) + +func setParentGID(_ *exec.Cmd) {} + +func killProc(cmd *exec.Cmd, devCommand string) { + // Credit: https://stackoverflow.com/a/44551450 + // For whatever reason, killing an npm script on windows just doesn't exit properly with cancel + if cmd != nil && cmd.Process != nil { + kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)) + var errorBuffer bytes.Buffer + var stdoutBuffer bytes.Buffer + kill.Stderr = &errorBuffer + kill.Stdout = &stdoutBuffer + err := kill.Run() + if err != nil { + if err.Error() != "exit status 1" { + println(stdoutBuffer.String()) + println(errorBuffer.String()) + logutils.LogRed("Error from '%s': %s", devCommand, err.Error()) + } + } + } +} diff --git a/v2/cmd/wails/internal/dev/stdout_scanner.go b/v2/cmd/wails/internal/dev/stdout_scanner.go new file mode 100644 index 000000000..dad4e72cf --- /dev/null +++ b/v2/cmd/wails/internal/dev/stdout_scanner.go @@ -0,0 +1,84 @@ +package dev + +import ( + "bufio" + "fmt" + "net/url" + "os" + "strings" + + "github.com/acarl005/stripansi" + "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "golang.org/x/mod/semver" +) + +// stdoutScanner acts as a stdout target that will scan the incoming +// data to find out the vite server url +type stdoutScanner struct { + ViteServerURLChan chan string + ViteServerVersionC chan string + versionDetected bool +} + +// NewStdoutScanner creates a new stdoutScanner +func NewStdoutScanner() *stdoutScanner { + return &stdoutScanner{ + ViteServerURLChan: make(chan string, 2), + ViteServerVersionC: make(chan string, 2), + } +} + +// Write bytes to the scanner. Will copy the bytes to stdout +func (s *stdoutScanner) Write(data []byte) (n int, err error) { + input := stripansi.Strip(string(data)) + if !s.versionDetected { + v, err := detectViteVersion(input) + if v != "" || err != nil { + if err != nil { + logutils.LogRed("ViteStdoutScanner: %s", err) + v = "v0.0.0" + } + s.ViteServerVersionC <- v + s.versionDetected = true + } + } + + match := strings.Index(input, "Local:") + if match != -1 { + sc := bufio.NewScanner(strings.NewReader(input)) + for sc.Scan() { + line := sc.Text() + index := strings.Index(line, "Local:") + if index == -1 || len(line) < 7 { + continue + } + viteServerURL := strings.TrimSpace(line[index+6:]) + logutils.LogGreen("Vite Server URL: %s", viteServerURL) + _, err := url.Parse(viteServerURL) + if err != nil { + logutils.LogRed(err.Error()) + } else { + s.ViteServerURLChan <- viteServerURL + } + } + } + return os.Stdout.Write(data) +} + +func detectViteVersion(line string) (string, error) { + s := strings.Split(strings.TrimSpace(line), " ") + if strings.ToLower(s[0]) != "vite" { + return "", nil + } + + if len(line) < 2 { + return "", fmt.Errorf("unable to parse vite version") + } + + v := s[1] + if !semver.IsValid(v) { + return "", fmt.Errorf("%s is not a valid vite version string", v) + } + + return v, nil +} diff --git a/v2/cmd/wails/internal/dev/watcher.go b/v2/cmd/wails/internal/dev/watcher.go new file mode 100644 index 000000000..e1161f87c --- /dev/null +++ b/v2/cmd/wails/internal/dev/watcher.go @@ -0,0 +1,78 @@ +package dev + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "github.com/wailsapp/wails/v2/internal/fs" + + "github.com/fsnotify/fsnotify" + gitignore "github.com/sabhiram/go-gitignore" + "github.com/samber/lo" +) + +type Watcher interface { + Add(name string) error +} + +// initialiseWatcher creates the project directory watcher that will trigger recompile +func initialiseWatcher(cwd, reloadDirs string) (*fsnotify.Watcher, error) { + // Ignore dot files, node_modules and build directories by default + ignoreDirs := getIgnoreDirs(cwd) + + // Get all subdirectories + dirs, err := fs.GetSubdirectories(cwd) + if err != nil { + return nil, err + } + + customDirs := dirs.AsSlice() + seperatedDirs := strings.Split(reloadDirs, ",") + for _, dir := range seperatedDirs { + customSub, err := fs.GetSubdirectories(filepath.Join(cwd, dir)) + if err != nil { + return nil, err + } + customDirs = append(customDirs, customSub.AsSlice()...) + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + for _, dir := range processDirectories(customDirs, ignoreDirs) { + err := watcher.Add(dir) + if err != nil { + return nil, err + } + } + return watcher, nil +} + +func getIgnoreDirs(cwd string) []string { + ignoreDirs := []string{filepath.Join(cwd, "build/*"), ".*", "node_modules"} + baseDir := filepath.Base(cwd) + // Read .gitignore into ignoreDirs + f, err := os.Open(filepath.Join(cwd, ".gitignore")) + if err == nil { + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if line != baseDir { + ignoreDirs = append(ignoreDirs, line) + } + } + } + + return lo.Uniq(ignoreDirs) +} + +func processDirectories(dirs []string, ignoreDirs []string) []string { + ignorer := gitignore.CompileIgnoreLines(ignoreDirs...) + return lo.Filter(dirs, func(dir string, _ int) bool { + return !ignorer.MatchesPath(dir) + }) +} diff --git a/v2/cmd/wails/internal/dev/watcher_test.go b/v2/cmd/wails/internal/dev/watcher_test.go new file mode 100644 index 000000000..ad228b66c --- /dev/null +++ b/v2/cmd/wails/internal/dev/watcher_test.go @@ -0,0 +1,113 @@ +package dev + +import ( + "github.com/samber/lo" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wailsapp/wails/v2/internal/fs" +) + +func Test_processDirectories(t *testing.T) { + tests := []struct { + name string + dirs []string + ignoreDirs []string + want []string + }{ + { + name: "Should ignore .git", + ignoreDirs: []string{".git"}, + dirs: []string{".git", "some/path/to/nested/.git", "some/path/to/nested/.git/CHANGELOG"}, + want: []string{}, + }, + { + name: "Should ignore node_modules", + ignoreDirs: []string{"node_modules"}, + dirs: []string{"node_modules", "path/to/node_modules", "path/to/node_modules/some/other/path"}, + want: []string{}, + }, + { + name: "Should ignore dirs starting with .", + ignoreDirs: []string{".*"}, + dirs: []string{".test", ".gitignore", ".someother", "valid"}, + want: []string{"valid"}, + }, + { + name: "Should ignore dirs in ignoreDirs", + dirs: []string{"build", "build/my.exe", "build/my.app"}, + ignoreDirs: []string{"build"}, + want: []string{}, + }, + { + name: "Should ignore subdirectories", + dirs: []string{"build", "some/path/to/build", "some/path/to/CHANGELOG", "some/other/path"}, + ignoreDirs: []string{"some/**/*"}, + want: []string{"build"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := processDirectories(tt.dirs, tt.ignoreDirs) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("processDirectories() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_GetIgnoreDirs(t *testing.T) { + + // Remove testdir if it exists + _ = os.RemoveAll("testdir") + + tests := []struct { + name string + files []string + want []string + shouldErr bool + }{ + { + name: "Should have defaults", + files: []string{}, + want: []string{"testdir/build/*", ".*", "node_modules"}, + }, + { + name: "Should ignore dotFiles", + files: []string{".test1", ".wailsignore"}, + want: []string{"testdir/build/*", ".*", "node_modules"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file + err := fs.Mkdir("testdir") + require.NoError(t, err) + defer func() { + err := os.RemoveAll("testdir") + require.NoError(t, err) + }() + for _, file := range tt.files { + fs.MustWriteString(filepath.Join("testdir", file), "") + } + + got := getIgnoreDirs("testdir") + + got = lo.Map(got, func(s string, _ int) string { + return filepath.ToSlash(s) + }) + + if (err != nil) != tt.shouldErr { + t.Errorf("initialiseWatcher() error = %v, shouldErr %v", err, tt.shouldErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("initialiseWatcher() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/cmd/wails/internal/gomod/gomod.go b/v2/cmd/wails/internal/gomod/gomod.go new file mode 100644 index 000000000..5da14a5ff --- /dev/null +++ b/v2/cmd/wails/internal/gomod/gomod.go @@ -0,0 +1,68 @@ +package gomod + +import ( + "fmt" + "os" + "strings" + + "github.com/wailsapp/wails/v2/cmd/wails/internal" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/gomod" + "github.com/wailsapp/wails/v2/internal/goversion" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +func SyncGoMod(logger *clilogger.CLILogger, updateWailsVersion bool) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + gomodFilename := fs.FindFileInParents(cwd, "go.mod") + if gomodFilename == "" { + return fmt.Errorf("no go.mod file found") + } + gomodData, err := os.ReadFile(gomodFilename) + if err != nil { + return err + } + + gomodData, updated, err := gomod.SyncGoVersion(gomodData, goversion.MinRequirement) + if err != nil { + return err + } else if updated { + LogGreen("Updated go.mod to use Go '%s'", goversion.MinRequirement) + } + + internalVersion := strings.TrimSpace(internal.Version) + if outOfSync, err := gomod.GoModOutOfSync(gomodData, internalVersion); err != nil { + return err + } else if outOfSync { + if updateWailsVersion { + LogGreen("Updating go.mod to use Wails '%s'", internalVersion) + gomodData, err = gomod.UpdateGoModVersion(gomodData, internalVersion) + if err != nil { + return err + } + updated = true + } else { + gomodversion, err := gomod.GetWailsVersionFromModFile(gomodData) + if err != nil { + return err + } + + logger.Println("Warning: go.mod is using Wails '%s' but the CLI is '%s'. Consider updating your project's `go.mod` file.\n", gomodversion.String(), internal.Version) + } + } + + if updated { + return os.WriteFile(gomodFilename, gomodData, 0o755) + } + + return nil +} + +func LogGreen(message string, args ...interface{}) { + text := fmt.Sprintf(message, args...) + println(colour.Green(text)) +} diff --git a/v2/cmd/wails/internal/logutils/color-logs.go b/v2/cmd/wails/internal/logutils/color-logs.go new file mode 100644 index 000000000..65553df3f --- /dev/null +++ b/v2/cmd/wails/internal/logutils/color-logs.go @@ -0,0 +1,31 @@ +package logutils + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/colour" +) + +func LogGreen(message string, args ...interface{}) { + if len(message) == 0 { + return + } + text := fmt.Sprintf(message, args...) + println(colour.Green(text)) +} + +func LogRed(message string, args ...interface{}) { + if len(message) == 0 { + return + } + text := fmt.Sprintf(message, args...) + println(colour.Red(text)) +} + +func LogDarkYellow(message string, args ...interface{}) { + if len(message) == 0 { + return + } + text := fmt.Sprintf(message, args...) + println(colour.DarkYellow(text)) +} diff --git a/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template b/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template new file mode 100644 index 000000000..5363d10f2 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template @@ -0,0 +1,41 @@ +# Next Steps + +Congratulations on generating your template! + +## Completing your template + +The next steps to complete the template are: + + 1. Complete the fields in the `template.json` file. + - It is really important to ensure `helpurl` is valid as this is where users of the template will be directed for help. + 2. Update `README.md`. + 3. Edit `wails.json` and ensure all fields are correct, especially: + - `wailsjsdir` - path to generate wailsjs modules + - `frontend:install` - The command to install your frontend dependencies + - `frontend:build` - The command to build your frontend + 4. Remove any `public` or `dist` directories. + 5. Delete this file. + +## Testing your template + +You can test your template by running this command: + +`wails init -n test -t {{.TemplateDir}}` + +### Checklist + +Once generated, do the following tests: + - Change into the new project directory and run `wails build`. A working binary should be generated in the `build/bin` project directory. + - Run `wails dev`. This will compile your app and run it. + - You should be able to see your application running on http://localhost:34115/ + +## Publishing your template + +You can publish a template to a git repository and use it as follows: + +`wails init -name test -t https://your/git/url` + +EG: + +`wails init -name test -t https://github.com/leaanthony/testtemplate` + diff --git a/v2/cmd/wails/internal/template/base/README.md b/v2/cmd/wails/internal/template/base/README.md new file mode 100644 index 000000000..ed259fcff --- /dev/null +++ b/v2/cmd/wails/internal/template/base/README.md @@ -0,0 +1,15 @@ +# README + +## About + +About your template + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. In another terminal, go into the `frontend` +directory and run `npm run dev`. The frontend dev server will run on http://localhost:34115. Connect to this in your +browser and connect to your application. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/cmd/wails/internal/template/base/app.tmpl.go b/v2/cmd/wails/internal/template/base/app.tmpl.go new file mode 100644 index 000000000..224be7156 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/app.tmpl.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called at application startup +func (a *App) startup(ctx context.Context) { + // Perform your setup here + a.ctx = ctx +} + +// domReady is called after front-end resources have been loaded +func (a App) domReady(ctx context.Context) { + // Add your action here +} + +// beforeClose is called when the application is about to quit, +// either by clicking the window close button or calling runtime.Quit. +// Returning true will cause the application to continue, false will continue shutdown as normal. +func (a *App) beforeClose(ctx context.Context) (prevent bool) { + return false +} + +// shutdown is called at application termination +func (a *App) shutdown(ctx context.Context) { + // Perform your teardown here +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png b/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png new file mode 100644 index 000000000..27ef13655 Binary files /dev/null and b/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png differ diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/index.html b/v2/cmd/wails/internal/template/base/frontend/dist/index.html new file mode 100644 index 000000000..e2a14c1b5 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/frontend/dist/index.html @@ -0,0 +1,20 @@ + + + + + + + + + +
+ +
Please enter your name below 👇
+
+ + +
+
+ + + diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/main.css b/v2/cmd/wails/internal/template/base/frontend/dist/main.css new file mode 100644 index 000000000..f35a69f99 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/frontend/dist/main.css @@ -0,0 +1,79 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} + +.logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-image: url("./assets/images/logo-universal.png"); + background-size: 100% 100%; + background-origin: content-box; +} +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/main.js b/v2/cmd/wails/internal/template/base/frontend/dist/main.js new file mode 100644 index 000000000..98510cd39 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/frontend/dist/main.js @@ -0,0 +1,32 @@ +// Get input + focus +let nameElement = document.getElementById("name"); +nameElement.focus(); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + window.go.main.App.Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +nameElement.onkeydown = function (e) { + if (e.keyCode == 13) { + window.greet(); + } +}; diff --git a/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json b/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json new file mode 100644 index 000000000..01780288d --- /dev/null +++ b/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json @@ -0,0 +1,12 @@ +{ + "name": "{{.ProjectName}}", + "version": "1.0.0", + "description": "", + "main": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "" + }, + "keywords": [], + "author": "{{.AuthorName}}" +} diff --git a/v2/cmd/wails/internal/template/base/go.sum b/v2/cmd/wails/internal/template/base/go.sum new file mode 100644 index 000000000..92f4d6d57 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/go.sum @@ -0,0 +1,180 @@ +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI= +github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= +github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= +github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= +github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+aPcuJ5Y/hpjq/lmgMFmpjwl0= +github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js= +github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A= +github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs= +github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe h1:FiWQ7XhDkc4zAH8SEx1BTte/6VHyceraUusH8jf5SQw= +github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= +github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/cmd/wails/internal/template/base/go.tmpl.mod b/v2/cmd/wails/internal/template/base/go.tmpl.mod new file mode 100644 index 000000000..42478753c --- /dev/null +++ b/v2/cmd/wails/internal/template/base/go.tmpl.mod @@ -0,0 +1,32 @@ +module changeme + + go 1.18 + + require github.com/wailsapp/wails/v2 {{.WailsVersion}} + + require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.7.2 // indirect + github.com/labstack/gommon v0.3.1 // indirect + github.com/leaanthony/go-ansi-parser v1.0.1 // indirect + github.com/leaanthony/gosod v1.0.3 // indirect + github.com/leaanthony/slicer v1.5.0 // indirect + github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/tkrajina/go-reflector v0.5.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + golang.org/x/text v0.3.7 // indirect + ) + + // replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/cmd/wails/internal/template/base/main.go.tmpl b/v2/cmd/wails/internal/template/base/main.go.tmpl new file mode 100644 index 000000000..d8e902027 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/main.go.tmpl @@ -0,0 +1,87 @@ +package main + +import ( + "embed" + "log" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/logger" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/mac" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed build/appicon.png +var icon []byte + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + MinWidth: 1024, + MinHeight: 768, + MaxWidth: 1280, + MaxHeight: 800, + DisableResize: false, + Fullscreen: false, + Frameless: false, + StartHidden: false, + HideWindowOnClose: false, + BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255}, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + Menu: nil, + Logger: nil, + LogLevel: logger.DEBUG, + OnStartup: app.startup, + OnDomReady: app.domReady, + OnBeforeClose: app.beforeClose, + OnShutdown: app.shutdown, + WindowStartState: options.Normal, + Bind: []interface{}{ + app, + }, + // Windows platform specific options + Windows: &windows.Options{ + WebviewIsTransparent: false, + WindowIsTranslucent: false, + DisableWindowIcon: false, + // DisableFramelessWindowDecorations: false, + WebviewUserDataPath: "", + ZoomFactor: 1.0, + }, + // Mac platform specific options + Mac: &mac.Options{ + TitleBar: &mac.TitleBar{ + TitlebarAppearsTransparent: true, + HideTitle: false, + HideTitleBar: false, + FullSizeContent: false, + UseToolbar: false, + HideToolbarSeparator: true, + }, + Appearance: mac.NSAppearanceNameDarkAqua, + WebviewIsTransparent: true, + WindowIsTranslucent: true, + About: &mac.AboutInfo{ + Title: "{{.ProjectName}}", + Message: "", + Icon: icon, + }, + }, + }) + + if err != nil { + log.Fatal(err) + } +} diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh new file mode 100644 index 000000000..bc6ee0acb --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Start building the app for macos platform..." +wails build --clean --platform darwin/arm64 + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh new file mode 100644 index 000000000..f359f633a --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Start building the app for macos platform..." +wails build --clean --platform darwin + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos.sh new file mode 100644 index 000000000..d61531fd7 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/build-macos.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Start building the app for macos platform..." +wails build --clean --platform darwin/universal + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/scripts/build-windows.sh b/v2/cmd/wails/internal/template/base/scripts/build-windows.sh new file mode 100644 index 000000000..47b778970 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/build-windows.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Start building the app for windows platform..." +wails build --clean --platform windows/amd64 + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/scripts/build.sh b/v2/cmd/wails/internal/template/base/scripts/build.sh new file mode 100644 index 000000000..20ab7eb21 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/build.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Start building the app..." +wails build --clean + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh b/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh new file mode 100644 index 000000000..7539d8e33 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +echo -e "Start running the script..." +cd ../ + +echo -e "Current Go version: \c" +go version + +echo -e "Install the Wails command line tool..." +go install github.com/wailsapp/wails/v2/cmd/wails@latest + +echo -e "Successful installation!" + +echo -e "End running the script!" diff --git a/v2/cmd/wails/internal/template/base/template.json.template b/v2/cmd/wails/internal/template/base/template.json.template new file mode 100644 index 000000000..bde089e00 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/template.json.template @@ -0,0 +1,7 @@ +{ + "name": "Long name", + "shortname": "{{.Name}}", + "author": "", + "description": "Description of the template", + "helpurl": "URL for help with the template, eg homepage" +} \ No newline at end of file diff --git a/v2/cmd/wails/internal/template/base/wails.tmpl.json b/v2/cmd/wails/internal/template/base/wails.tmpl.json new file mode 100644 index 000000000..cdb10e346 --- /dev/null +++ b/v2/cmd/wails/internal/template/base/wails.tmpl.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/cmd/wails/internal/template/template.go b/v2/cmd/wails/internal/template/template.go new file mode 100644 index 000000000..6b4937db6 --- /dev/null +++ b/v2/cmd/wails/internal/template/template.go @@ -0,0 +1,8 @@ +package template + +import ( + "embed" +) + +//go:embed base +var Base embed.FS diff --git a/v2/cmd/wails/internal/version.go b/v2/cmd/wails/internal/version.go new file mode 100644 index 000000000..cfc37182c --- /dev/null +++ b/v2/cmd/wails/internal/version.go @@ -0,0 +1,6 @@ +package internal + +import _ "embed" + +//go:embed version.txt +var Version string diff --git a/v2/cmd/wails/internal/version.txt b/v2/cmd/wails/internal/version.txt new file mode 100644 index 000000000..805579f30 --- /dev/null +++ b/v2/cmd/wails/internal/version.txt @@ -0,0 +1 @@ +v2.11.0 \ No newline at end of file diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go new file mode 100644 index 000000000..ccf1576e9 --- /dev/null +++ b/v2/cmd/wails/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/internal" + + "github.com/wailsapp/wails/v2/internal/colour" + + "github.com/leaanthony/clir" +) + +func banner(_ *clir.Cli) string { + return fmt.Sprintf("%s %s", + colour.Green("Wails CLI"), + colour.DarkRed(internal.Version)) +} + +func fatal(message string) { + printer := pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalPrefixStyle, + Text: " FATAL ", + }, + } + printer.Println(message) + os.Exit(1) +} + +func printBulletPoint(text string, args ...any) { + item := pterm.BulletListItem{ + Level: 2, + Text: text, + } + t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender() + if err != nil { + fatal(err.Error()) + } + t = strings.Trim(t, "\n\r") + pterm.Printfln(t, args...) +} + +func printFooter() { + printer := pterm.PrefixPrinter{ + MessageStyle: pterm.NewStyle(pterm.FgLightGreen), + Prefix: pterm.Prefix{ + Style: pterm.NewStyle(pterm.FgRed, pterm.BgLightWhite), + Text: "♥ ", + }, + } + printer.Println("If Wails is useful to you or your company, please consider sponsoring the project:") + pterm.Println("https://github.com/sponsors/leaanthony") +} + +func bool2Str(b bool) string { + if b { + return "true" + } + return "false" +} + +var app *clir.Cli + +func main() { + var err error + + app = clir.NewCli("Wails", "Go/HTML Appkit", internal.Version) + + app.SetBannerFunction(banner) + defer printFooter() + + app.NewSubCommandFunction("build", "Builds the application", buildApplication) + app.NewSubCommandFunction("dev", "Runs the application in development mode", devApplication) + app.NewSubCommandFunction("doctor", "Diagnose your environment", diagnoseEnvironment) + app.NewSubCommandFunction("init", "Initialises a new Wails project", initProject) + app.NewSubCommandFunction("update", "Update the Wails CLI", update) + + show := app.NewSubCommand("show", "Shows various information") + show.NewSubCommandFunction("releasenotes", "Shows the release notes for the current version", showReleaseNotes) + + generate := app.NewSubCommand("generate", "Code Generation Tools") + generate.NewSubCommandFunction("module", "Generates a new Wails module", generateModule) + generate.NewSubCommandFunction("template", "Generates a new Wails template", generateTemplate) + + command := app.NewSubCommand("version", "The Wails CLI version") + command.Action(func() error { + pterm.Println(internal.Version) + return nil + }) + + err = app.Run() + if err != nil { + pterm.Println() + pterm.Error.Println(err.Error()) + printFooter() + os.Exit(1) + } +} diff --git a/v2/cmd/wails/show.go b/v2/cmd/wails/show.go new file mode 100644 index 000000000..c83900c8d --- /dev/null +++ b/v2/cmd/wails/show.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/github" +) + +func showReleaseNotes(f *flags.ShowReleaseNotes) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + version := internal.Version + if f.Version != "" { + version = f.Version + } + + app.PrintBanner() + releaseNotes := github.GetReleaseNotes(version, f.NoColour) + pterm.Println(releaseNotes) + + return nil +} diff --git a/v2/cmd/wails/update.go b/v2/cmd/wails/update.go new file mode 100644 index 000000000..9f8b6e604 --- /dev/null +++ b/v2/cmd/wails/update.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + "os" + + "github.com/labstack/gommon/color" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/shell" + + "github.com/wailsapp/wails/v2/internal/github" +) + +// AddSubcommand adds the `init` command for the Wails application +func update(f *flags.Update) error { + if f.NoColour { + colour.ColourEnabled = false + pterm.DisableColor() + + } + // Print banner + app.PrintBanner() + pterm.Println("Checking for updates...") + + var desiredVersion *github.SemanticVersion + var err error + var valid bool + + if len(f.Version) > 0 { + // Check if this is a valid version + valid, err = github.IsValidTag(f.Version) + if err == nil { + if !valid { + err = fmt.Errorf("version '%s' is invalid", f.Version) + } else { + desiredVersion, err = github.NewSemanticVersion(f.Version) + } + } + } else { + if f.PreRelease { + desiredVersion, err = github.GetLatestPreRelease() + } else { + desiredVersion, err = github.GetLatestStableRelease() + if err != nil { + pterm.Println("") + pterm.Println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:") + pterm.Println(" wails update -pre") + return nil + } + } + } + if err != nil { + return err + } + pterm.Println() + + pterm.Println(" Current Version : " + app.Version()) + + if len(f.Version) > 0 { + fmt.Printf(" Desired Version : v%s\n", desiredVersion) + } else { + if f.PreRelease { + fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion) + } else { + fmt.Printf(" Latest Release : v%s\n", desiredVersion) + } + } + + return updateToVersion(desiredVersion, len(f.Version) > 0, app.Version()) +} + +func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentVersion string) error { + targetVersionString := "v" + targetVersion.String() + + if targetVersionString == currentVersion { + pterm.Println("\nLooks like you're up to date!") + return nil + } + + var desiredVersion string + + if !force { + + compareVersion := currentVersion + + currentVersion, err := github.NewSemanticVersion(compareVersion) + if err != nil { + return err + } + + var success bool + + // Release -> Pre-Release = Massage current version to prerelease format + if targetVersion.IsPreRelease() && currentVersion.IsRelease() { + testVersion, err := github.NewSemanticVersion(compareVersion + "-0") + if err != nil { + return err + } + success, _ = targetVersion.IsGreaterThan(testVersion) + } + // Pre-Release -> Release = Massage target version to prerelease format + if targetVersion.IsRelease() && currentVersion.IsPreRelease() { + // We are ok with greater than or equal + mainversion := currentVersion.MainVersion() + targetVersion, err = github.NewSemanticVersion(targetVersion.String()) + if err != nil { + return err + } + success, _ = targetVersion.IsGreaterThanOrEqual(mainversion) + } + + // Release -> Release = Standard check + if (targetVersion.IsRelease() && currentVersion.IsRelease()) || + (targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) { + + success, _ = targetVersion.IsGreaterThan(currentVersion) + } + + // Compare + if !success { + pterm.Println("Error: The requested version is lower than the current version.") + pterm.Println(fmt.Sprintf("If this is what you really want to do, use `wails update -version "+"%s`", targetVersionString)) + + return nil + } + + desiredVersion = "v" + targetVersion.String() + + } else { + desiredVersion = "v" + targetVersion.String() + } + + pterm.Println() + pterm.Print("Installing Wails CLI " + desiredVersion + "...") + + // Run command in non module directory + homeDir, err := os.UserHomeDir() + if err != nil { + fatal("Cannot find home directory! Please file a bug report!") + } + + sout, serr, err := shell.RunCommand(homeDir, "go", "install", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion) + if err != nil { + pterm.Println("Failed.") + pterm.Error.Println(sout + `\n` + serr) + return err + } + pterm.Println("Done.") + pterm.Println(color.Green("\nMake sure you update your project go.mod file to use " + desiredVersion + ":")) + pterm.Println(color.Green(" require github.com/wailsapp/wails/v2 " + desiredVersion)) + pterm.Println(color.Red("\nTo view the release notes, please run `wails show releasenotes`")) + + return nil +} diff --git a/v2/examples/customlayout/.gitignore b/v2/examples/customlayout/.gitignore new file mode 100644 index 000000000..53e9ed8b5 --- /dev/null +++ b/v2/examples/customlayout/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +myfrontend/wailsjs \ No newline at end of file diff --git a/v2/examples/customlayout/README.md b/v2/examples/customlayout/README.md new file mode 100644 index 000000000..e4d79d4ec --- /dev/null +++ b/v2/examples/customlayout/README.md @@ -0,0 +1,4 @@ +# README + +This is an example project that shows how to use a custom layout. +Run `wails build` in the `cmd/customlayout` directory to build the project. \ No newline at end of file diff --git a/v2/examples/customlayout/build/README.md b/v2/examples/customlayout/build/README.md new file mode 100644 index 000000000..3018a06c4 --- /dev/null +++ b/v2/examples/customlayout/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/v2/examples/customlayout/build/appicon.png b/v2/examples/customlayout/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v2/examples/customlayout/build/appicon.png differ diff --git a/v2/examples/customlayout/build/darwin/Info.dev.plist b/v2/examples/customlayout/build/darwin/Info.dev.plist new file mode 100644 index 000000000..02e7358ee --- /dev/null +++ b/v2/examples/customlayout/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.Name}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/v2/examples/customlayout/build/darwin/Info.plist b/v2/examples/customlayout/build/darwin/Info.plist new file mode 100644 index 000000000..e7819a7e8 --- /dev/null +++ b/v2/examples/customlayout/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.Name}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + + \ No newline at end of file diff --git a/v2/examples/customlayout/build/windows/icon.ico b/v2/examples/customlayout/build/windows/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/v2/examples/customlayout/build/windows/icon.ico differ diff --git a/v2/examples/customlayout/build/windows/info.json b/v2/examples/customlayout/build/windows/info.json new file mode 100644 index 000000000..c23c173c9 --- /dev/null +++ b/v2/examples/customlayout/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/v2/examples/customlayout/build/windows/installer/project.nsi b/v2/examples/customlayout/build/windows/installer/project.nsi new file mode 100644 index 000000000..2ccc0f3f3 --- /dev/null +++ b/v2/examples/customlayout/build/windows/installer/project.nsi @@ -0,0 +1,104 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v2/examples/customlayout/build/windows/installer/wails_tools.nsh b/v2/examples/customlayout/build/windows/installer/wails_tools.nsh new file mode 100644 index 000000000..66dc209a3 --- /dev/null +++ b/v2/examples/customlayout/build/windows/installer/wails_tools.nsh @@ -0,0 +1,171 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend \ No newline at end of file diff --git a/cmd/packages/windows/wails.exe.manifest b/v2/examples/customlayout/build/windows/wails.exe.manifest similarity index 61% rename from cmd/packages/windows/wails.exe.manifest rename to v2/examples/customlayout/build/windows/wails.exe.manifest index b236d268f..17e1a2387 100644 --- a/cmd/packages/windows/wails.exe.manifest +++ b/v2/examples/customlayout/build/windows/wails.exe.manifest @@ -1,12 +1,15 @@ - - + + + + + + true/pm permonitorv2,permonitor - true \ No newline at end of file diff --git a/v2/examples/customlayout/cmd/customlayout/app.go b/v2/examples/customlayout/cmd/customlayout/app.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/examples/customlayout/cmd/customlayout/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/examples/customlayout/cmd/customlayout/main.go b/v2/examples/customlayout/cmd/customlayout/main.go new file mode 100644 index 000000000..dcb59a80c --- /dev/null +++ b/v2/examples/customlayout/cmd/customlayout/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "changeme/myfrontend" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "customlayout", + Width: 1024, + Height: 768, + Assets: myfrontend.Assets, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/customlayout/cmd/customlayout/wails.json b/v2/examples/customlayout/cmd/customlayout/wails.json new file mode 100644 index 000000000..e37c2ec7d --- /dev/null +++ b/v2/examples/customlayout/cmd/customlayout/wails.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "customlayout", + "outputfilename": "customlayout", + "build:dir": "../../build", + "frontend:dir": "../../myfrontend", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/examples/customlayout/go.mod b/v2/examples/customlayout/go.mod new file mode 100644 index 000000000..e1a17304e --- /dev/null +++ b/v2/examples/customlayout/go.mod @@ -0,0 +1,39 @@ +module changeme + +go 1.22.0 + +toolchain go1.24.1 + +require github.com/wailsapp/wails/v2 v2.1.0 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +replace github.com/wailsapp/wails/v2 v2.1.0 => ../.. diff --git a/v2/examples/customlayout/go.sum b/v2/examples/customlayout/go.sum new file mode 100644 index 000000000..f1995affb --- /dev/null +++ b/v2/examples/customlayout/go.sum @@ -0,0 +1,111 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= +github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= +github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= +github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= +github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w= +github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= +github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/examples/customlayout/myfrontend/assets.go b/v2/examples/customlayout/myfrontend/assets.go new file mode 100644 index 000000000..a6dec2f8f --- /dev/null +++ b/v2/examples/customlayout/myfrontend/assets.go @@ -0,0 +1,6 @@ +package myfrontend + +import "embed" + +//go:embed all:dist +var Assets embed.FS diff --git a/v2/examples/customlayout/myfrontend/index.html b/v2/examples/customlayout/myfrontend/index.html new file mode 100644 index 000000000..1ceda7392 --- /dev/null +++ b/v2/examples/customlayout/myfrontend/index.html @@ -0,0 +1,12 @@ + + + + + + customlayout + + +
+ + + diff --git a/v2/examples/customlayout/myfrontend/package.json b/v2/examples/customlayout/myfrontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/examples/customlayout/myfrontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/examples/customlayout/myfrontend/src/app.css b/v2/examples/customlayout/myfrontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/examples/customlayout/myfrontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt b/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png b/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png differ diff --git a/v2/examples/customlayout/myfrontend/src/main.js b/v2/examples/customlayout/myfrontend/src/main.js new file mode 100644 index 000000000..6cb4ad78d --- /dev/null +++ b/v2/examples/customlayout/myfrontend/src/main.js @@ -0,0 +1,48 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import { Greet } from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; +document.addEventListener("keydown", (e) => { + if (e.code === "Enter") { + window.greet(); + } +}); + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; diff --git a/v2/examples/customlayout/myfrontend/src/style.css b/v2/examples/customlayout/myfrontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/examples/customlayout/myfrontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/examples/dragdrop-test/.gitignore b/v2/examples/dragdrop-test/.gitignore new file mode 100644 index 000000000..a11bbf414 --- /dev/null +++ b/v2/examples/dragdrop-test/.gitignore @@ -0,0 +1,4 @@ +build/bin +node_modules +frontend/dist +frontend/wailsjs diff --git a/v2/examples/dragdrop-test/README.md b/v2/examples/dragdrop-test/README.md new file mode 100644 index 000000000..397b08b92 --- /dev/null +++ b/v2/examples/dragdrop-test/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/examples/dragdrop-test/app.go b/v2/examples/dragdrop-test/app.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/examples/dragdrop-test/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/examples/dragdrop-test/build/README.md b/v2/examples/dragdrop-test/build/README.md new file mode 100644 index 000000000..1ae2f677f --- /dev/null +++ b/v2/examples/dragdrop-test/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/v2/examples/dragdrop-test/build/appicon.png b/v2/examples/dragdrop-test/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v2/examples/dragdrop-test/build/appicon.png differ diff --git a/v2/examples/dragdrop-test/build/darwin/Info.dev.plist b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist new file mode 100644 index 000000000..14121ef7c --- /dev/null +++ b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/v2/examples/dragdrop-test/build/darwin/Info.plist b/v2/examples/dragdrop-test/build/darwin/Info.plist new file mode 100644 index 000000000..d17a7475c --- /dev/null +++ b/v2/examples/dragdrop-test/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/v2/examples/dragdrop-test/build/windows/icon.ico b/v2/examples/dragdrop-test/build/windows/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/v2/examples/dragdrop-test/build/windows/icon.ico differ diff --git a/v2/examples/dragdrop-test/build/windows/info.json b/v2/examples/dragdrop-test/build/windows/info.json new file mode 100644 index 000000000..9727946b7 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/build/windows/installer/project.nsi b/v2/examples/dragdrop-test/build/windows/installer/project.nsi new file mode 100644 index 000000000..654ae2e49 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh new file mode 100644 index 000000000..f9c0f8852 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/v2/examples/dragdrop-test/build/windows/wails.exe.manifest b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest new file mode 100644 index 000000000..17e1a2387 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/index.html b/v2/examples/dragdrop-test/frontend/index.html new file mode 100644 index 000000000..4010f1be6 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + dragdrop-test + + +
+ + + diff --git a/v2/examples/dragdrop-test/frontend/package-lock.json b/v2/examples/dragdrop-test/frontend/package-lock.json new file mode 100644 index 000000000..8eed5313c --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/package-lock.json @@ -0,0 +1,653 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/v2/examples/dragdrop-test/frontend/package.json b/v2/examples/dragdrop-test/frontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/app.css b/v2/examples/dragdrop-test/frontend/src/app.css new file mode 100644 index 000000000..1d3b595bc --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/app.css @@ -0,0 +1,229 @@ +/* #app styles are in style.css to avoid conflicts */ + +.compact-container { + display: flex; + gap: 15px; + margin: 15px 0; + justify-content: center; + align-items: flex-start; +} + +.drag-source { + background: white; + border: 2px solid #5c6bc0; + padding: 12px; + min-width: 140px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.drag-source h4 { + color: #3949ab; + margin: 0 0 8px 0; + font-size: 14px; +} + +.draggable { + background: #f5f5f5; + color: #1a1a1a; + padding: 8px; + margin: 6px 0; + border-radius: 4px; + cursor: move; + text-align: center; + transition: all 0.3s ease; + font-weight: 600; + font-size: 14px; + border: 2px solid #c5cae9; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.draggable:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + background: #e8eaf6; + border-color: #7986cb; +} + +.draggable.dragging { + opacity: 0.5; + transform: scale(0.95); + background: #c5cae9; +} + +.drop-zone { + background: #f8f9fa; + border: 2px dashed #9e9e9e; + padding: 12px; + min-width: 180px; + min-height: 120px; + border-radius: 6px; + transition: all 0.3s ease; +} + +.drop-zone h4 { + color: #5c6bc0; + margin: 0 0 8px 0; + font-size: 14px; +} + +.drop-zone.drag-over { + background: #e3f2fd; + border-color: #2196F3; + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.2); +} + +.file-drop-zone { + background: #fff8e1; + border: 2px dashed #ffc107; + padding: 12px; + min-width: 180px; + min-height: 120px; + border-radius: 6px; + transition: all 0.3s ease; +} + +.file-drop-zone h4 { + color: #f57c00; + margin: 0 0 8px 0; + font-size: 14px; +} + +.file-drop-zone.drag-over { + background: #fff3cd; + border-color: #ff9800; + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(255, 152, 0, 0.2); +} + +.dropped-item { + background: linear-gradient(135deg, #42a5f5 0%, #66bb6a 100%); + color: white; + padding: 6px 8px; + margin: 4px 2px; + border-radius: 4px; + text-align: center; + animation: slideIn 0.3s ease; + display: inline-block; + font-weight: 500; + font-size: 13px; +} + +.dropped-file { + background: #fff; + border: 2px solid #ff9800; + color: #333; + padding: 6px 8px; + margin: 4px 0; + border-radius: 4px; + text-align: left; + animation: slideIn 0.3s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + font-size: 13px; +} + +#dropMessage, #fileDropMessage { + font-size: 12px; + color: #666; + margin: 4px 0; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.status { + margin: 15px auto; + max-width: 1000px; + padding: 12px; + background: #2c3e50; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.status h4 { + color: white; + margin: 0 0 8px 0; + font-size: 14px; +} + +#eventLog { + background: #1a1a1a; + padding: 10px; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + font-family: 'Courier New', monospace; + text-align: left; + font-size: 12px; +} + +.log-entry { + padding: 4px 8px; + font-size: 13px; + margin: 2px 0; + border-radius: 3px; +} + +.log-entry.drag-start { + color: #81c784; + background: rgba(129, 199, 132, 0.1); +} + +.log-entry.drag-over { + color: #64b5f6; + background: rgba(100, 181, 246, 0.1); +} + +.log-entry.drag-enter { + color: #ffb74d; + background: rgba(255, 183, 77, 0.1); +} + +.log-entry.drag-leave { + color: #ba68c8; + background: rgba(186, 104, 200, 0.1); +} + +.log-entry.drop { + color: #e57373; + background: rgba(229, 115, 115, 0.1); + font-weight: bold; +} + +.log-entry.drag-end { + color: #90a4ae; + background: rgba(144, 164, 174, 0.1); +} + +.log-entry.file-drop { + color: #ffc107; + background: rgba(255, 193, 7, 0.1); + font-weight: bold; +} + +.log-entry.page-loaded { + color: #4caf50; + background: rgba(76, 175, 80, 0.1); +} + +.log-entry.wails-status { + color: #00bcd4; + background: rgba(0, 188, 212, 0.1); +} + +h1 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-size: 1.8em; + margin: 10px 0 8px 0; +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/examples/dragdrop-test/frontend/src/main.js b/v2/examples/dragdrop-test/frontend/src/main.js new file mode 100644 index 000000000..60d76ac0f --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/main.js @@ -0,0 +1,231 @@ +import './style.css'; +import './app.css'; + +// CRITICAL: Register global handlers IMMEDIATELY to prevent file drops from opening new windows +// This must be done as early as possible, before any other code runs +(function() { + // Helper function to check if drag event contains files + function isFileDrop(e) { + return e.dataTransfer && e.dataTransfer.types && + (e.dataTransfer.types.includes('Files') || + Array.from(e.dataTransfer.types).includes('Files')); + } + + // Global dragover handler - MUST prevent default for file drops + window.addEventListener('dragover', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + } + }, true); // Use capture phase to handle before any other handlers + + // Global drop handler - MUST prevent default for file drops + window.addEventListener('drop', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + console.log('Global handler prevented file drop navigation'); + } + }, true); // Use capture phase to handle before any other handlers + + // Global dragleave handler + window.addEventListener('dragleave', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + } + }, true); // Use capture phase + + console.log('Global file drop prevention handlers registered'); +})(); + +document.querySelector('#app').innerHTML = ` +

Wails Drag & Drop Test

+ +
+
+

HTML5 Source

+
Item 1
+
Item 2
+
Item 3
+
+ +
+

HTML5 Drop

+

Drop here

+
+ +
+

File Drop

+

Drop files here

+
+
+ +
+

Event Log

+
+
+`; + +// Get all draggable items and drop zones +const draggables = document.querySelectorAll('.draggable'); +const dropZone = document.getElementById('dropZone'); +const fileDropZone = document.getElementById('fileDropZone'); +const eventLog = document.getElementById('eventLog'); +const dropMessage = document.getElementById('dropMessage'); +const fileDropMessage = document.getElementById('fileDropMessage'); + +let draggedItem = null; +let eventCounter = 0; + +// Function to log events +function logEvent(eventName, details = '') { + eventCounter++; + const timestamp = new Date().toLocaleTimeString(); + const logEntry = document.createElement('div'); + logEntry.className = `log-entry ${eventName.replace(' ', '-').toLowerCase()}`; + logEntry.textContent = `[${timestamp}] ${eventCounter}. ${eventName} ${details}`; + eventLog.insertBefore(logEntry, eventLog.firstChild); + + // Keep only last 20 events + while (eventLog.children.length > 20) { + eventLog.removeChild(eventLog.lastChild); + } + + console.log(`Event: ${eventName} ${details}`); +} + +// Add event listeners to draggable items +draggables.forEach(item => { + // Drag start + item.addEventListener('dragstart', (e) => { + draggedItem = e.target; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'copy'; + e.dataTransfer.setData('text/plain', e.target.dataset.item); + logEvent('drag-start', `- Started dragging: ${e.target.dataset.item}`); + }); + + // Drag end + item.addEventListener('dragend', (e) => { + e.target.classList.remove('dragging'); + logEvent('drag-end', `- Ended dragging: ${e.target.dataset.item}`); + }); +}); + +// Add event listeners to HTML drop zone +dropZone.addEventListener('dragenter', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + logEvent('drag-enter', '- Entered HTML drop zone'); +}); + +dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + // Don't log every dragover to avoid spam +}); + +dropZone.addEventListener('dragleave', (e) => { + if (e.target === dropZone) { + dropZone.classList.remove('drag-over'); + logEvent('drag-leave', '- Left HTML drop zone'); + } +}); + +dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + + const data = e.dataTransfer.getData('text/plain'); + logEvent('drop', `- Dropped in HTML zone: ${data}`); + + if (draggedItem) { + // Create a copy of the dragged item + const droppedElement = document.createElement('div'); + droppedElement.className = 'dropped-item'; + droppedElement.textContent = data; + + // Remove the placeholder message if it exists + if (dropMessage) { + dropMessage.style.display = 'none'; + } + + dropZone.appendChild(droppedElement); + } + + draggedItem = null; +}); + +// Add event listeners to file drop zone +fileDropZone.addEventListener('dragenter', (e) => { + e.preventDefault(); + fileDropZone.classList.add('drag-over'); + logEvent('drag-enter', '- Entered file drop zone'); +}); + +fileDropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; +}); + +fileDropZone.addEventListener('dragleave', (e) => { + if (e.target === fileDropZone) { + fileDropZone.classList.remove('drag-over'); + logEvent('drag-leave', '- Left file drop zone'); + } +}); + +fileDropZone.addEventListener('drop', (e) => { + e.preventDefault(); + fileDropZone.classList.remove('drag-over'); + + const files = [...e.dataTransfer.files]; + if (files.length > 0) { + logEvent('file-drop', `- Dropped ${files.length} file(s)`); + + // Hide the placeholder message + if (fileDropMessage) { + fileDropMessage.style.display = 'none'; + } + + // Display dropped files + files.forEach(file => { + const fileElement = document.createElement('div'); + fileElement.className = 'dropped-file'; + + // Format file size + let size = file.size; + let unit = 'bytes'; + if (size > 1024 * 1024) { + size = (size / (1024 * 1024)).toFixed(2); + unit = 'MB'; + } else if (size > 1024) { + size = (size / 1024).toFixed(2); + unit = 'KB'; + } + + fileElement.textContent = `📄 ${file.name} (${size} ${unit})`; + fileDropZone.appendChild(fileElement); + }); + } +}); + +// Log when page loads +window.addEventListener('DOMContentLoaded', () => { + logEvent('page-loaded', '- Wails drag-and-drop test page ready'); + console.log('Wails Drag and Drop test application loaded'); + + // Check if Wails drag and drop is enabled + if (window.wails && window.wails.flags) { + logEvent('wails-status', `- Wails DnD enabled: ${window.wails.flags.enableWailsDragAndDrop}`); + } + + // IMPORTANT: Register Wails drag-and-drop handlers to prevent browser navigation + // This will ensure external files don't open in new windows when dropped anywhere + if (window.runtime && window.runtime.OnFileDrop) { + window.runtime.OnFileDrop((x, y, paths) => { + logEvent('wails-file-drop', `- Wails received ${paths.length} file(s) at (${x}, ${y})`); + console.log('Wails OnFileDrop:', paths); + }, false); // false = don't require drop target, handle all file drops + logEvent('wails-setup', '- Wails OnFileDrop handlers registered'); + } +}); \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/style.css b/v2/examples/dragdrop-test/frontend/src/style.css new file mode 100644 index 000000000..f5d071597 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/style.css @@ -0,0 +1,33 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; + height: 100%; + overflow: hidden; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + height: 100%; + overflow: hidden; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; + overflow: hidden; + box-sizing: border-box; + padding: 10px; +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..02a3bb988 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..c71ae77cb --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..4445dac21 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..7cb89d750 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/go.mod b/v2/examples/dragdrop-test/go.mod new file mode 100644 index 000000000..be13aac19 --- /dev/null +++ b/v2/examples/dragdrop-test/go.mod @@ -0,0 +1,37 @@ +module dragdrop-test + +go 1.23 + +require github.com/wailsapp/wails/v2 v2.10.1 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +replace github.com/wailsapp/wails/v2 => E:/releases/wails/v2 diff --git a/v2/examples/dragdrop-test/go.sum b/v2/examples/dragdrop-test/go.sum new file mode 100644 index 000000000..10d4a9b18 --- /dev/null +++ b/v2/examples/dragdrop-test/go.sum @@ -0,0 +1,79 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/examples/dragdrop-test/main.go b/v2/examples/dragdrop-test/main.go new file mode 100644 index 000000000..64a0c2734 --- /dev/null +++ b/v2/examples/dragdrop-test/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "Wails Drag & Drop Test", + Width: 800, + Height: 600, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/dragdrop-test/wails.json b/v2/examples/dragdrop-test/wails.json new file mode 100644 index 000000000..7970ea4ca --- /dev/null +++ b/v2/examples/dragdrop-test/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "dragdrop-test", + "outputfilename": "dragdrop-test", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/examples/panic-recovery-test/README.md b/v2/examples/panic-recovery-test/README.md new file mode 100644 index 000000000..c0a6a7e5a --- /dev/null +++ b/v2/examples/panic-recovery-test/README.md @@ -0,0 +1,76 @@ +# Panic Recovery Test + +This example demonstrates the Linux signal handler issue (#3965) and verifies the fix using `runtime.ResetSignalHandlers()`. + +## The Problem + +On Linux, WebKit installs signal handlers without the `SA_ONSTACK` flag, which prevents Go from recovering panics caused by nil pointer dereferences (SIGSEGV). Without the fix, the application crashes with: + +``` +signal 11 received but handler not on signal stack +fatal error: non-Go code set up signal handler without SA_ONSTACK flag +``` + +## The Solution + +Call `runtime.ResetSignalHandlers()` immediately before code that might panic: + +```go +import "github.com/wailsapp/wails/v2/pkg/runtime" + +go func() { + defer func() { + if err := recover(); err != nil { + log.Printf("Recovered: %v", err) + } + }() + runtime.ResetSignalHandlers() + // Code that might panic... +}() +``` + +## How to Reproduce + +### Prerequisites + +- Linux with WebKit2GTK 4.1 installed +- Go 1.21+ +- Wails CLI + +### Steps + +1. Build the example: + ```bash + cd v2/examples/panic-recovery-test + wails build -tags webkit2_41 + ``` + +2. Run the application: + ```bash + ./build/bin/panic-recovery-test + ``` + +3. Wait ~10 seconds (the app auto-calls `Greet` after 5s, then waits another 5s before the nil pointer dereference) + +### Expected Result (with fix) + +The panic is recovered and you see: +``` +------------------------------"invalid memory address or nil pointer dereference" +``` + +The application continues running. + +### Without the fix + +Comment out the `runtime.ResetSignalHandlers()` call in `app.go` and rebuild. The application will crash with a fatal signal 11 error. + +## Files + +- `app.go` - Contains the `Greet` function that demonstrates panic recovery +- `frontend/src/main.js` - Auto-calls `Greet` after 5 seconds to trigger the test + +## Related + +- Issue: https://github.com/wailsapp/wails/issues/3965 +- Original fix PR: https://github.com/wailsapp/wails/pull/2152 diff --git a/v2/examples/panic-recovery-test/app.go b/v2/examples/panic-recovery-test/app.go new file mode 100644 index 000000000..ceb46e8d5 --- /dev/null +++ b/v2/examples/panic-recovery-test/app.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + go func() { + defer func() { + if err := recover(); err != nil { + fmt.Printf("------------------------------%#v\n", err) + } + }() + time.Sleep(5 * time.Second) + // Fix signal handlers right before potential panic using the Wails runtime + runtime.ResetSignalHandlers() + // Nil pointer dereference - causes SIGSEGV + var t *time.Time + fmt.Println(t.Unix()) + }() + + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/examples/panic-recovery-test/frontend/index.html b/v2/examples/panic-recovery-test/frontend/index.html new file mode 100644 index 000000000..d7aa4e942 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + panic-test + + +
+ + + diff --git a/v2/examples/panic-recovery-test/frontend/package.json b/v2/examples/panic-recovery-test/frontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/src/app.css b/v2/examples/panic-recovery-test/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/examples/panic-recovery-test/frontend/src/main.js b/v2/examples/panic-recovery-test/frontend/src/main.js new file mode 100644 index 000000000..ea5e74fc6 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/main.js @@ -0,0 +1,55 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +// Auto-call Greet after 5 seconds to trigger the panic test +setTimeout(() => { + console.log("Auto-calling Greet to trigger panic test..."); + Greet("PanicTest") + .then((result) => { + resultElement.innerText = result + " (auto-called - panic will occur in 5s)"; + }) + .catch((err) => { + console.error("Error:", err); + }); +}, 5000); diff --git a/v2/examples/panic-recovery-test/frontend/src/style.css b/v2/examples/panic-recovery-test/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 000000000..02a3bb988 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js new file mode 100755 index 000000000..c71ae77cb --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..4445dac21 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..7cb89d750 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/go.mod b/v2/examples/panic-recovery-test/go.mod new file mode 100644 index 000000000..026042cbf --- /dev/null +++ b/v2/examples/panic-recovery-test/go.mod @@ -0,0 +1,5 @@ +module panic-recovery-test + +go 1.21 + +require github.com/wailsapp/wails/v2 v2.11.0 diff --git a/v2/examples/panic-recovery-test/main.go b/v2/examples/panic-recovery-test/main.go new file mode 100644 index 000000000..f6a38e86c --- /dev/null +++ b/v2/examples/panic-recovery-test/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "panic-test", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/panic-recovery-test/wails.json b/v2/examples/panic-recovery-test/wails.json new file mode 100644 index 000000000..56770f091 --- /dev/null +++ b/v2/examples/panic-recovery-test/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "panic-recovery-test", + "outputfilename": "panic-recovery-test", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 000000000..2eb753ee2 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,112 @@ +module github.com/wailsapp/wails/v2 + +go 1.22.0 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d + github.com/bep/debounce v1.2.1 + github.com/bitfield/script v0.24.0 + github.com/charmbracelet/glamour v0.8.0 + github.com/flytam/filenamify v1.2.0 + github.com/fsnotify/fsnotify v1.9.0 + github.com/go-git/go-git/v5 v5.13.2 + github.com/go-ole/go-ole v1.3.0 + github.com/godbus/dbus/v5 v5.1.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/jackmordaunt/icns v1.0.0 + github.com/jaypipes/ghw v0.21.3 + github.com/labstack/echo/v4 v4.13.3 + github.com/labstack/gommon v0.4.2 + github.com/leaanthony/clir v1.3.0 + github.com/leaanthony/debme v1.2.1 + github.com/leaanthony/go-ansi-parser v1.6.1 + github.com/leaanthony/gosod v1.0.4 + github.com/leaanthony/slicer v1.6.0 + github.com/leaanthony/u v1.1.1 + github.com/leaanthony/winicon v1.0.0 + github.com/matryer/is v1.4.1 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/pkg/errors v0.9.1 + github.com/pterm/pterm v0.12.80 + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 + github.com/samber/lo v1.49.1 + github.com/stretchr/testify v1.10.0 + github.com/tc-hib/winres v0.3.1 + github.com/tidwall/sjson v1.2.5 + github.com/tkrajina/go-reflector v0.5.8 + github.com/wailsapp/go-webview2 v1.0.22 + github.com/wailsapp/mimetype v1.4.1 + github.com/wzshiming/ctc v1.2.3 + golang.org/x/mod v0.23.0 + golang.org/x/net v0.35.0 + golang.org/x/sys v0.30.0 + golang.org/x/tools v0.30.0 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.12.1 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/itchyny/gojq v0.12.13 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/jaypipes/pcidb v1.1.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect + github.com/tidwall/gjson v1.14.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/image v0.12.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect + mvdan.cc/sh/v3 v3.7.0 // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 000000000..f6df3507e --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,351 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bitfield/script v0.24.0 h1:ic0Tbx+2AgRtkGGIcUyr+Un60vu4WXvqFrCSumf+T7M= +github.com/bitfield/script v0.24.0/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= +github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I= +github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/flytam/filenamify v1.2.0 h1:7RiSqXYR4cJftDQ5NuvljKMfd/ubKnW/j9C6iekChgI= +github.com/flytam/filenamify v1.2.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= +github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= +github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= +github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8= +github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE= +github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro= +github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/clir v1.3.0 h1:L9nPDWrmc/qU9UWZZvRaFajWYuO0np9V5p+5gxyYno0= +github.com/leaanthony/clir v1.3.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= +github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= +github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4= +github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c= +github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= +golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw= +howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/v2/internal/app/app.go b/v2/internal/app/app.go new file mode 100644 index 000000000..0cd6bf614 --- /dev/null +++ b/v2/internal/app/app.go @@ -0,0 +1,45 @@ +package app + +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/menumanager" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" +) + +// App defines a Wails application structure +type App struct { + frontend frontend.Frontend + logger *logger.Logger + options *options.App + + menuManager *menumanager.Manager + + // Indicates if the app is in debug mode + debug bool + + // Indicates if the devtools is enabled + devtoolsEnabled bool + + // OnStartup/OnShutdown + startupCallback func(ctx context.Context) + shutdownCallback func(ctx context.Context) + ctx context.Context +} + +// Shutdown the application +func (a *App) Shutdown() { + if a.frontend != nil { + a.frontend.Quit() + } +} + +// SetApplicationMenu sets the application menu +func (a *App) SetApplicationMenu(menu *menu.Menu) { + if a.frontend != nil { + a.frontend.MenuSetApplicationMenu(menu) + } +} diff --git a/v2/internal/app/app_bindings.go b/v2/internal/app/app_bindings.go new file mode 100644 index 000000000..be031819c --- /dev/null +++ b/v2/internal/app/app_bindings.go @@ -0,0 +1,124 @@ +//go:build bindings + +package app + +import ( + "flag" + "os" + "path/filepath" + + "github.com/leaanthony/gosod" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend/runtime/wrapper" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (a *App) Run() error { + + // Create binding exemptions - Ugly hack. There must be a better way + bindingExemptions := []interface{}{ + a.options.OnStartup, + a.options.OnShutdown, + a.options.OnDomReady, + a.options.OnBeforeClose, + } + + // Check for CLI Flags + bindingFlags := flag.NewFlagSet("bindings", flag.ContinueOnError) + + var tsPrefixFlag *string + var tsPostfixFlag *string + var tsOutputTypeFlag *string + + tsPrefix := os.Getenv("tsprefix") + if tsPrefix == "" { + tsPrefixFlag = bindingFlags.String("tsprefix", "", "Prefix for generated typescript entities") + } + + tsSuffix := os.Getenv("tssuffix") + if tsSuffix == "" { + tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities") + } + + tsOutputType := os.Getenv("tsoutputtype") + if tsOutputType == "" { + tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)") + } + + _ = bindingFlags.Parse(os.Args[1:]) + if tsPrefixFlag != nil { + tsPrefix = *tsPrefixFlag + } + if tsPostfixFlag != nil { + tsSuffix = *tsPostfixFlag + } + if tsOutputTypeFlag != nil { + tsOutputType = *tsOutputTypeFlag + } + + appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind) + + appBindings.SetTsPrefix(tsPrefix) + appBindings.SetTsSuffix(tsSuffix) + appBindings.SetOutputType(tsOutputType) + + err := generateBindings(appBindings) + if err != nil { + return err + } + return nil +} + +// CreateApp creates the app! +func CreateApp(appoptions *options.App) (*App, error) { + // Set up logger + myLogger := logger.New(appoptions.Logger) + myLogger.SetLogLevel(appoptions.LogLevel) + + result := &App{ + logger: myLogger, + options: appoptions, + } + + return result, nil + +} + +func generateBindings(bindings *binding.Bindings) error { + + cwd, err := os.Getwd() + if err != nil { + return err + } + projectConfig, err := project.Load(cwd) + if err != nil { + return err + } + + wailsjsbasedir := filepath.Join(projectConfig.GetWailsJSDir(), "wailsjs") + + runtimeDir := filepath.Join(wailsjsbasedir, "runtime") + _ = os.RemoveAll(runtimeDir) + extractor := gosod.New(wrapper.RuntimeWrapper) + err = extractor.Extract(runtimeDir, nil) + if err != nil { + return err + } + + goBindingsDir := filepath.Join(wailsjsbasedir, "go") + err = os.RemoveAll(goBindingsDir) + if err != nil { + return err + } + _ = fs.MkDirs(goBindingsDir) + + err = bindings.GenerateGoBindings(goBindingsDir) + if err != nil { + return err + } + + return fs.SetPermissions(wailsjsbasedir, 0755) +} diff --git a/v2/internal/app/app_debug.go b/v2/internal/app/app_debug.go new file mode 100644 index 000000000..c14bedec1 --- /dev/null +++ b/v2/internal/app/app_debug.go @@ -0,0 +1,7 @@ +//go:build debug + +package app + +func IsDebug() bool { + return true +} diff --git a/v2/internal/app/app_debug_not.go b/v2/internal/app/app_debug_not.go new file mode 100644 index 000000000..04f841ede --- /dev/null +++ b/v2/internal/app/app_debug_not.go @@ -0,0 +1,7 @@ +//go:build !debug + +package app + +func IsDebug() bool { + return false +} diff --git a/v2/internal/app/app_default_unix.go b/v2/internal/app/app_default_unix.go new file mode 100644 index 000000000..10d801285 --- /dev/null +++ b/v2/internal/app/app_default_unix.go @@ -0,0 +1,18 @@ +//go:build !dev && !production && !bindings && (linux || darwin) + +package app + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (a *App) Run() error { + return nil +} + +// CreateApp creates the app! +func CreateApp(_ *options.App) (*App, error) { + return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`) +} diff --git a/v2/internal/app/app_default_windows.go b/v2/internal/app/app_default_windows.go new file mode 100644 index 000000000..b1b66a081 --- /dev/null +++ b/v2/internal/app/app_default_windows.go @@ -0,0 +1,27 @@ +//go:build !dev && !production && !bindings && windows + +package app + +import ( + "os/exec" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (a *App) Run() error { + return nil +} + +// CreateApp creates the app! +func CreateApp(_ *options.App) (*App, error) { + result := w32.MessageBox(0, + `Wails applications will not build without the correct build tags. +Please use "wails build" or press "OK" to open the documentation on how to use "go build"`, + "Error", + w32.MB_ICONERROR|w32.MB_OKCANCEL) + if result == 1 { + exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://wails.io/docs/guides/manual-builds").Start() + } + return nil, nil +} diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go new file mode 100644 index 000000000..6de845f96 --- /dev/null +++ b/v2/internal/app/app_dev.go @@ -0,0 +1,298 @@ +//go:build dev + +package app + +import ( + "context" + "embed" + "flag" + "fmt" + iofs "io/fs" + "net" + "net/url" + "os" + "path/filepath" + "time" + + "github.com/wailsapp/wails/v2/pkg/assetserver" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend/desktop" + "github.com/wailsapp/wails/v2/internal/frontend/devserver" + "github.com/wailsapp/wails/v2/internal/frontend/dispatcher" + "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/menumanager" + pkglogger "github.com/wailsapp/wails/v2/pkg/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (a *App) Run() error { + err := a.frontend.Run(a.ctx) + a.frontend.RunMainLoop() + a.frontend.WindowClose() + if a.shutdownCallback != nil { + a.shutdownCallback(a.ctx) + } + return err +} + +// CreateApp creates the app! +func CreateApp(appoptions *options.App) (*App, error) { + var err error + + ctx := context.Background() + ctx = context.WithValue(ctx, "debug", true) + ctx = context.WithValue(ctx, "devtoolsEnabled", true) + + // Set up logger if the appoptions.LogLevel is an invalid value, set it to the default log level + appoptions.LogLevel, err = pkglogger.StringToLogLevel(appoptions.LogLevel.String()) + if err != nil { + return nil, err + } + + myLogger := logger.New(appoptions.Logger) + myLogger.SetLogLevel(appoptions.LogLevel) + + // Check for CLI Flags + devFlags := flag.NewFlagSet("dev", flag.ContinueOnError) + + var assetdirFlag *string + var devServerFlag *string + var frontendDevServerURLFlag *string + var loglevelFlag *string + + assetdir := os.Getenv("assetdir") + if assetdir == "" { + assetdirFlag = devFlags.String("assetdir", "", "Directory to serve assets") + } + + devServer := os.Getenv("devserver") + if devServer == "" { + devServerFlag = devFlags.String("devserver", "", "Address to bind the wails dev server to") + } + + frontendDevServerURL := os.Getenv("frontenddevserverurl") + if frontendDevServerURL == "" { + frontendDevServerURLFlag = devFlags.String("frontenddevserverurl", "", "URL of the external frontend dev server") + } + + loglevel := os.Getenv("loglevel") + appLogLevel := appoptions.LogLevel.String() + if loglevel != "" { + appLogLevel = loglevel + } + loglevelFlag = devFlags.String("loglevel", appLogLevel, "Loglevel to use - Trace, Debug, Info, Warning, Error") + + // If we weren't given the assetdir in the environment variables + if assetdir == "" { + // Parse args but ignore errors in case -appargs was used to pass in args for the app. + _ = devFlags.Parse(os.Args[1:]) + if assetdirFlag != nil { + assetdir = *assetdirFlag + } + if devServerFlag != nil { + devServer = *devServerFlag + } + if frontendDevServerURLFlag != nil { + frontendDevServerURL = *frontendDevServerURLFlag + } + if loglevelFlag != nil { + loglevel = *loglevelFlag + } + } + + assetConfig, err := assetserver.BuildAssetServerConfig(appoptions) + if err != nil { + return nil, err + } + + if assetConfig.Assets == nil && frontendDevServerURL != "" { + myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.") + frontendDevServerURL = "" + assetdir = "" + } + + if frontendDevServerURL != "" { + _, port, err := net.SplitHostPort(devServer) + if err != nil { + return nil, fmt.Errorf("unable to determine port of DevServer: %s", err) + } + + ctx = context.WithValue(ctx, "assetserverport", port) + + ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL) + + externalURL, err := url.Parse(frontendDevServerURL) + if err != nil { + return nil, err + } + + if externalURL.Host == "" { + return nil, fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?") + } + + waitCb := func() { myLogger.Debug("Waiting for frontend DevServer '%s' to be ready", externalURL) } + if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) { + myLogger.Error("Timeout waiting for frontend DevServer") + } + + handler := assetserver.NewExternalAssetsHandler(myLogger, assetConfig, externalURL) + assetConfig.Assets = nil + assetConfig.Handler = handler + assetConfig.Middleware = nil + + myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL) + } else { + if assetdir == "" { + // If no assetdir has been defined, let's try to infer it from the project root and the asset FS. + assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets) + if err != nil { + return nil, fmt.Errorf("unable to infer the AssetDir from your Assets fs.FS: %w", err) + } + } + + if assetdir != "" { + // Let's override the assets to serve from on disk, if needed + absdir, err := filepath.Abs(assetdir) + if err != nil { + return nil, err + } + + myLogger.Info("Serving assets from disk: %s", absdir) + assetConfig.Assets = os.DirFS(absdir) + + ctx = context.WithValue(ctx, "assetdir", assetdir) + } + } + + // Migrate deprecated options to the new AssetServer option + appoptions.Assets = nil + appoptions.AssetsHandler = nil + appoptions.AssetServer = &assetConfig + + if devServer != "" { + ctx = context.WithValue(ctx, "devserver", devServer) + } + + if loglevel != "" { + level, err := pkglogger.StringToLogLevel(loglevel) + if err != nil { + return nil, err + } + // Only set the log level if it's different from the appoptions.LogLevel + if level != appoptions.LogLevel { + myLogger.SetLogLevel(level) + } + } + + // Attach logger to context + ctx = context.WithValue(ctx, "logger", myLogger) + ctx = context.WithValue(ctx, "buildtype", "dev") + + // Preflight checks + err = PreflightChecks(appoptions, myLogger) + if err != nil { + return nil, err + } + + // Merge default options + options.MergeDefaults(appoptions) + + var menuManager *menumanager.Manager + + // Process the application menu + if appoptions.Menu != nil { + // Create the menu manager + menuManager = menumanager.NewManager() + err = menuManager.SetApplicationMenu(appoptions.Menu) + if err != nil { + return nil, err + } + } + + // Create binding exemptions - Ugly hack. There must be a better way + bindingExemptions := []interface{}{ + appoptions.OnStartup, + appoptions.OnShutdown, + appoptions.OnDomReady, + appoptions.OnBeforeClose, + } + appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind) + + eventHandler := runtime.NewEvents(myLogger) + ctx = context.WithValue(ctx, "events", eventHandler) + messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery) + + // Create the frontends and register to event handler + desktopFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) + appFrontend := devserver.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher, menuManager, desktopFrontend) + eventHandler.AddFrontend(appFrontend) + eventHandler.AddFrontend(desktopFrontend) + + ctx = context.WithValue(ctx, "frontend", appFrontend) + result := &App{ + ctx: ctx, + frontend: appFrontend, + logger: myLogger, + menuManager: menuManager, + startupCallback: appoptions.OnStartup, + shutdownCallback: appoptions.OnShutdown, + debug: true, + devtoolsEnabled: true, + } + + result.options = appoptions + + return result, nil + +} + +func tryInferAssetDirFromFS(assets iofs.FS) (string, error) { + if _, isEmbedFs := assets.(embed.FS); !isEmbedFs { + // We only infer the assetdir for embed.FS assets + return "", nil + } + + path, err := fs.FindPathToFile(assets, "index.html") + if err != nil { + return "", err + } + + path, err = filepath.Abs(path) + if err != nil { + return "", err + } + + if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil { + if os.IsNotExist(err) { + err = fmt.Errorf( + "inferred assetdir '%s' does not exist or does not contain an 'index.html' file, "+ + "please specify it with -assetdir or set it in wails.json", + path) + } + return "", err + } + + return path, nil +} + +func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) { + if timeout == 0 { + timeout = time.Minute + } + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + conn, _ := net.DialTimeout("tcp", host, 2*time.Second) + if conn != nil { + conn.Close() + return true + } + + waitCB() + time.Sleep(1 * time.Second) + } + return false +} diff --git a/v2/internal/app/app_devtools.go b/v2/internal/app/app_devtools.go new file mode 100644 index 000000000..60b221094 --- /dev/null +++ b/v2/internal/app/app_devtools.go @@ -0,0 +1,8 @@ +//go:build devtools + +package app + +// Note: devtools flag is also added in debug builds +func IsDevtoolsEnabled() bool { + return true +} diff --git a/v2/internal/app/app_devtools_not.go b/v2/internal/app/app_devtools_not.go new file mode 100644 index 000000000..912672048 --- /dev/null +++ b/v2/internal/app/app_devtools_not.go @@ -0,0 +1,9 @@ +//go:build !devtools + +package app + +// IsDevtoolsEnabled returns true if devtools should be enabled +// Note: devtools flag is also added in debug builds +func IsDevtoolsEnabled() bool { + return false +} diff --git a/v2/internal/app/app_obfuscated.go b/v2/internal/app/app_obfuscated.go new file mode 100644 index 000000000..c78c10b87 --- /dev/null +++ b/v2/internal/app/app_obfuscated.go @@ -0,0 +1,8 @@ +//go:build obfuscated + +package app + +// IsObfuscated returns true if the obfuscated build tag is set +func IsObfuscated() bool { + return true +} diff --git a/v2/internal/app/app_obfuscated_not.go b/v2/internal/app/app_obfuscated_not.go new file mode 100644 index 000000000..90cc8559f --- /dev/null +++ b/v2/internal/app/app_obfuscated_not.go @@ -0,0 +1,8 @@ +//go:build !obfuscated + +package app + +// IsObfuscated returns false if the obfuscated build tag is not set +func IsObfuscated() bool { + return false +} diff --git a/v2/internal/app/app_preflight_unix.go b/v2/internal/app/app_preflight_unix.go new file mode 100644 index 000000000..f554df740 --- /dev/null +++ b/v2/internal/app/app_preflight_unix.go @@ -0,0 +1,12 @@ +//go:build (linux || darwin) && !bindings + +package app + +import ( + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func PreflightChecks(_ *options.App, _ *logger.Logger) error { + return nil +} diff --git a/v2/internal/app/app_preflight_windows.go b/v2/internal/app/app_preflight_windows.go new file mode 100644 index 000000000..1b71b8b19 --- /dev/null +++ b/v2/internal/app/app_preflight_windows.go @@ -0,0 +1,27 @@ +//go:build windows && !bindings + +package app + +import ( + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/wv2installer" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func PreflightChecks(options *options.App, logger *logger.Logger) error { + + _ = options + + // Process the webview2 runtime situation. We can pass a strategy in via the `webview2` flag for `wails build`. + // This will determine how wv2runtime.Process will handle a lack of valid runtime. + installedVersion, err := wv2installer.Process(options) + if installedVersion != "" { + logger.Debug("WebView2 Runtime Version '%s' installed. Minimum version required: %s.", + installedVersion, wv2installer.MinimumRuntimeVersion) + } + if err != nil { + return err + } + + return nil +} diff --git a/v2/internal/app/app_production.go b/v2/internal/app/app_production.go new file mode 100644 index 000000000..9eb0e5a66 --- /dev/null +++ b/v2/internal/app/app_production.go @@ -0,0 +1,104 @@ +//go:build production + +package app + +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend/desktop" + "github.com/wailsapp/wails/v2/internal/frontend/dispatcher" + "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/menumanager" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (a *App) Run() error { + err := a.frontend.Run(a.ctx) + a.frontend.RunMainLoop() + a.frontend.WindowClose() + if a.shutdownCallback != nil { + a.shutdownCallback(a.ctx) + } + return err +} + +// CreateApp creates the app! +func CreateApp(appoptions *options.App) (*App, error) { + var err error + + ctx := context.Background() + + // Merge default options + options.MergeDefaults(appoptions) + + debug := IsDebug() + devtoolsEnabled := IsDevtoolsEnabled() + ctx = context.WithValue(ctx, "debug", debug) + ctx = context.WithValue(ctx, "devtoolsEnabled", devtoolsEnabled) + + // Set up logger + myLogger := logger.New(appoptions.Logger) + if IsDebug() { + myLogger.SetLogLevel(appoptions.LogLevel) + } else { + myLogger.SetLogLevel(appoptions.LogLevelProduction) + } + ctx = context.WithValue(ctx, "logger", myLogger) + ctx = context.WithValue(ctx, "obfuscated", IsObfuscated()) + + // Preflight Checks + err = PreflightChecks(appoptions, myLogger) + if err != nil { + return nil, err + } + + // Create the menu manager + menuManager := menumanager.NewManager() + + // Process the application menu + if appoptions.Menu != nil { + err = menuManager.SetApplicationMenu(appoptions.Menu) + if err != nil { + return nil, err + } + } + + // Create binding exemptions - Ugly hack. There must be a better way + bindingExemptions := []interface{}{ + appoptions.OnStartup, + appoptions.OnShutdown, + appoptions.OnDomReady, + appoptions.OnBeforeClose, + } + appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated(), appoptions.EnumBind) + eventHandler := runtime.NewEvents(myLogger) + ctx = context.WithValue(ctx, "events", eventHandler) + // Attach logger to context + if debug { + ctx = context.WithValue(ctx, "buildtype", "debug") + } else { + ctx = context.WithValue(ctx, "buildtype", "production") + } + + messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery) + appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) + eventHandler.AddFrontend(appFrontend) + + ctx = context.WithValue(ctx, "frontend", appFrontend) + result := &App{ + ctx: ctx, + frontend: appFrontend, + logger: myLogger, + menuManager: menuManager, + startupCallback: appoptions.OnStartup, + shutdownCallback: appoptions.OnShutdown, + debug: debug, + devtoolsEnabled: devtoolsEnabled, + options: appoptions, + } + + return result, nil + +} diff --git a/v2/internal/binding/binding.go b/v2/internal/binding/binding.go new file mode 100644 index 000000000..b7bf07ae0 --- /dev/null +++ b/v2/internal/binding/binding.go @@ -0,0 +1,384 @@ +package binding + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strings" + + "github.com/wailsapp/wails/v2/internal/typescriptify" + + "github.com/leaanthony/slicer" + + "github.com/wailsapp/wails/v2/internal/logger" +) + +type Bindings struct { + db *DB + logger logger.CustomLogger + exemptions slicer.StringSlicer + + structsToGenerateTS map[string]map[string]interface{} + enumsToGenerateTS map[string]map[string]interface{} + tsPrefix string + tsSuffix string + tsInterface bool + obfuscate bool +} + +// NewBindings returns a new Bindings object +func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings { + result := &Bindings{ + db: newDB(), + logger: logger.CustomLogger("Bindings"), + structsToGenerateTS: make(map[string]map[string]interface{}), + enumsToGenerateTS: make(map[string]map[string]interface{}), + obfuscate: obfuscate, + } + + for _, exemption := range exemptions { + if exemption == nil { + continue + } + name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name() + // Yuk yuk yuk! Is there a better way? + name = strings.TrimSuffix(name, "-fm") + result.exemptions.Add(name) + } + + for _, enum := range enumsToBind { + result.AddEnumToGenerateTS(enum) + } + + // Add the structs to bind + for _, ptr := range structPointersToBind { + err := result.Add(ptr) + if err != nil { + logger.Fatal("Error during binding: " + err.Error()) + } + } + + return result +} + +// Add the given struct methods to the Bindings +func (b *Bindings) Add(structPtr interface{}) error { + methods, err := b.getMethods(structPtr) + if err != nil { + return fmt.Errorf("cannot bind value to app: %s", err.Error()) + } + + for _, method := range methods { + b.db.AddMethod(method.Path.Package, method.Path.Struct, method.Path.Name, method) + } + return nil +} + +func (b *Bindings) DB() *DB { + return b.db +} + +func (b *Bindings) ToJSON() (string, error) { + return b.db.ToJSON() +} + +func (b *Bindings) GenerateModels() ([]byte, error) { + models := map[string]string{} + var seen slicer.StringSlicer + var seenEnumsPackages slicer.StringSlicer + allStructNames := b.getAllStructNames() + allStructNames.Sort() + allEnumNames := b.getAllEnumNames() + allEnumNames.Sort() + for packageName, structsToGenerate := range b.structsToGenerateTS { + thisPackageCode := "" + w := typescriptify.New() + w.WithPrefix(b.tsPrefix) + w.WithSuffix(b.tsSuffix) + w.WithInterface(b.tsInterface) + w.Namespace = packageName + w.WithBackupDir("") + w.KnownStructs = allStructNames + w.KnownEnums = allEnumNames + // sort the structs + var structNames []string + for structName := range structsToGenerate { + structNames = append(structNames, structName) + } + sort.Strings(structNames) + for _, structName := range structNames { + fqstructname := packageName + "." + structName + if seen.Contains(fqstructname) { + continue + } + structInterface := structsToGenerate[structName] + w.Add(structInterface) + } + + // if we have enums for this package, add them as well + var enums, enumsExist = b.enumsToGenerateTS[packageName] + if enumsExist { + // Sort the enum names first to make the output deterministic + sortedEnumNames := make([]string, 0, len(enums)) + for enumName := range enums { + sortedEnumNames = append(sortedEnumNames, enumName) + } + sort.Strings(sortedEnumNames) + + for _, enumName := range sortedEnumNames { + enum := enums[enumName] + fqemumname := packageName + "." + enumName + if seen.Contains(fqemumname) { + continue + } + w.AddEnum(enum) + } + seenEnumsPackages.Add(packageName) + } + + str, err := w.Convert(nil) + if err != nil { + return nil, err + } + thisPackageCode += str + seen.AddSlice(w.GetGeneratedStructs()) + models[packageName] = thisPackageCode + } + + // Add outstanding enums to the models that were not in packages with structs + for packageName, enumsToGenerate := range b.enumsToGenerateTS { + if seenEnumsPackages.Contains(packageName) { + continue + } + + thisPackageCode := "" + w := typescriptify.New() + w.WithPrefix(b.tsPrefix) + w.WithSuffix(b.tsSuffix) + w.WithInterface(b.tsInterface) + w.Namespace = packageName + w.WithBackupDir("") + + for enumName, enum := range enumsToGenerate { + fqemumname := packageName + "." + enumName + if seen.Contains(fqemumname) { + continue + } + w.AddEnum(enum) + } + str, err := w.Convert(nil) + if err != nil { + return nil, err + } + thisPackageCode += str + models[packageName] = thisPackageCode + } + + // Sort the package names first to make the output deterministic + sortedPackageNames := make([]string, 0, len(models)) + for packageName := range models { + sortedPackageNames = append(sortedPackageNames, packageName) + } + sort.Strings(sortedPackageNames) + + var modelsData bytes.Buffer + for _, packageName := range sortedPackageNames { + modelData := models[packageName] + if strings.TrimSpace(modelData) == "" { + continue + } + modelsData.WriteString("export namespace " + packageName + " {\n") + sc := bufio.NewScanner(strings.NewReader(modelData)) + for sc.Scan() { + modelsData.WriteString("\t" + sc.Text() + "\n") + } + modelsData.WriteString("\n}\n\n") + } + return modelsData.Bytes(), nil +} + +func (b *Bindings) WriteModels(modelsDir string) error { + modelsData, err := b.GenerateModels() + if err != nil { + return err + } + // Don't write if we don't have anything + if len(modelsData) == 0 { + return nil + } + + filename := filepath.Join(modelsDir, "models.ts") + err = os.WriteFile(filename, modelsData, 0o755) + if err != nil { + return err + } + + return nil +} + +func (b *Bindings) AddEnumToGenerateTS(e interface{}) { + enumType := reflect.TypeOf(e) + + var packageName string + var enumName string + // enums should be represented as array of all possible values + if hasElements(enumType) { + enum := enumType.Elem() + // simple enum represented by struct with Value/TSName fields + if enum.Kind() == reflect.Struct { + _, tsNamePresented := enum.FieldByName("TSName") + enumT, valuePresented := enum.FieldByName("Value") + if tsNamePresented && valuePresented { + packageName = getPackageName(enumT.Type.String()) + enumName = enumT.Type.Name() + } else { + return + } + // otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname + } else { + packageName = getPackageName(enumType.Elem().String()) + enumName = enumType.Elem().Name() + } + if b.enumsToGenerateTS[packageName] == nil { + b.enumsToGenerateTS[packageName] = make(map[string]interface{}) + } + if b.enumsToGenerateTS[packageName][enumName] != nil { + return + } + b.enumsToGenerateTS[packageName][enumName] = e + } +} + +func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) { + if b.structsToGenerateTS[packageName] == nil { + b.structsToGenerateTS[packageName] = make(map[string]interface{}) + } + if b.structsToGenerateTS[packageName][structName] != nil { + return + } + b.structsToGenerateTS[packageName][structName] = s + + // Iterate this struct and add any struct field references + structType := reflect.TypeOf(s) + for hasElements(structType) { + structType = structType.Elem() + } + + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous || !field.IsExported() { + continue + } + kind := field.Type.Kind() + if kind == reflect.Struct { + fqname := field.Type.String() + sNameSplit := strings.SplitN(fqname, ".", 2) + if len(sNameSplit) < 2 { + continue + } + sName := sNameSplit[1] + pName := getPackageName(fqname) + a := reflect.New(field.Type) + if b.hasExportedJSONFields(field.Type) { + s := reflect.Indirect(a).Interface() + b.AddStructToGenerateTS(pName, sName, s) + } + } else { + fType := field.Type + for hasElements(fType) { + fType = fType.Elem() + } + if fType.Kind() == reflect.Struct { + fqname := fType.String() + sNameSplit := strings.SplitN(fqname, ".", 2) + if len(sNameSplit) < 2 { + continue + } + sName := sNameSplit[1] + pName := getPackageName(fqname) + a := reflect.New(fType) + if b.hasExportedJSONFields(fType) { + s := reflect.Indirect(a).Interface() + b.AddStructToGenerateTS(pName, sName, s) + } + } + } + } +} + +func (b *Bindings) SetTsPrefix(prefix string) *Bindings { + b.tsPrefix = prefix + return b +} + +func (b *Bindings) SetTsSuffix(postfix string) *Bindings { + b.tsSuffix = postfix + return b +} + +func (b *Bindings) SetOutputType(outputType string) *Bindings { + if outputType == "interfaces" { + b.tsInterface = true + } + return b +} + +func (b *Bindings) getAllStructNames() *slicer.StringSlicer { + var result slicer.StringSlicer + for packageName, structsToGenerate := range b.structsToGenerateTS { + for structName := range structsToGenerate { + result.Add(packageName + "." + structName) + } + } + return &result +} + +func (b *Bindings) getAllEnumNames() *slicer.StringSlicer { + var result slicer.StringSlicer + for packageName, enumsToGenerate := range b.enumsToGenerateTS { + for enumName := range enumsToGenerate { + result.Add(packageName + "." + enumName) + } + } + return &result +} + +func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool { + for i := 0; i < typeOf.NumField(); i++ { + jsonFieldName := "" + f := typeOf.Field(i) + // function, complex, and channel types cannot be json-encoded + if f.Type.Kind() == reflect.Chan || + f.Type.Kind() == reflect.Func || + f.Type.Kind() == reflect.UnsafePointer || + f.Type.Kind() == reflect.Complex128 || + f.Type.Kind() == reflect.Complex64 { + continue + } + jsonTag, hasTag := f.Tag.Lookup("json") + if !hasTag && f.IsExported() { + return true + } + if len(jsonTag) == 0 { + continue + } + jsonTagParts := strings.Split(jsonTag, ",") + if len(jsonTagParts) > 0 { + jsonFieldName = jsonTagParts[0] + } + for _, t := range jsonTagParts { + if t == "-" { + continue + } + } + if jsonFieldName != "" { + return true + } + } + return false +} diff --git a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go new file mode 100644 index 000000000..29777481b --- /dev/null +++ b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go @@ -0,0 +1,65 @@ +package binding_test + +type StructWithAnonymousSubMultiLevelStruct struct { + Name string `json:"name"` + Meta struct { + Age int `json:"age"` + More struct { + Info string `json:"info"` + MoreInMore struct { + Demo string `json:"demo"` + } `json:"more_in_more"` + } `json:"more"` + } `json:"meta"` +} + +func (s StructWithAnonymousSubMultiLevelStruct) Get() StructWithAnonymousSubMultiLevelStruct { + return s +} + +var AnonymousSubStructMultiLevelTest = BindingTest{ + name: "StructWithAnonymousSubMultiLevelStruct", + structs: []interface{}{ + &StructWithAnonymousSubMultiLevelStruct{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class StructWithAnonymousSubMultiLevelStruct { + name: string; + // Go type: struct { Age int "json:\"age\""; More struct { Info string "json:\"info\""; MoreInMore struct { Demo string "json:\"demo\"" } "json:\"more_in_more\"" } "json:\"more\"" } + meta: any; + + static createFrom(source: any = {}) { + return new StructWithAnonymousSubMultiLevelStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.meta = this.convertValues(source["meta"], Object); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go new file mode 100644 index 000000000..11afe4f0d --- /dev/null +++ b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go @@ -0,0 +1,59 @@ +package binding_test + +type StructWithAnonymousSubStruct struct { + Name string `json:"name"` + Meta struct { + Age int `json:"age"` + } `json:"meta"` +} + +func (s StructWithAnonymousSubStruct) Get() StructWithAnonymousSubStruct { + return s +} + +var AnonymousSubStructTest = BindingTest{ + name: "StructWithAnonymousSubStruct", + structs: []interface{}{ + &StructWithAnonymousSubStruct{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class StructWithAnonymousSubStruct { + name: string; + // Go type: struct { Age int "json:\"age\"" } + meta: any; + + static createFrom(source: any = {}) { + return new StructWithAnonymousSubStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.meta = this.convertValues(source["meta"], Object); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go new file mode 100644 index 000000000..b37334ec3 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go @@ -0,0 +1,64 @@ +package binding_test + +import ( + "io/fs" + "os" + "testing" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/float_package" + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/int_package" + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/map_package" + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/uint_package" + "github.com/wailsapp/wails/v2/internal/logger" +) + +const expectedBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {float_package} from '../models'; +import {int_package} from '../models'; +import {map_package} from '../models'; +import {uint_package} from '../models'; + +export function StartingWithFloat(arg1:float_package.SomeStruct):Promise; + +export function StartingWithInt(arg1:int_package.SomeStruct):Promise; + +export function StartingWithMap(arg1:map_package.SomeStruct):Promise; + +export function StartingWithUint(arg1:uint_package.SomeStruct):Promise; +` + +type HandlerTest struct{} + +func (h *HandlerTest) StartingWithInt(_ int_package.SomeStruct) {} +func (h *HandlerTest) StartingWithFloat(_ float_package.SomeStruct) {} +func (h *HandlerTest) StartingWithUint(_ uint_package.SomeStruct) {} +func (h *HandlerTest) StartingWithMap(_ map_package.SomeStruct) {} + +func TestConflictingPackageName(t *testing.T) { + // given + generationDir := t.TempDir() + + // setup + testLogger := &logger.Logger{} + b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false, []interface{}{}) + + // then + err := b.GenerateGoBindings(generationDir) + if err != nil { + t.Fatalf("could not generate the Go bindings: %v", err) + } + + // then + rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/HandlerTest.d.ts") + if err != nil { + t.Fatalf("could not read the generated bindings: %v", err) + } + + // then + generatedBindings := string(rawGeneratedBindings) + if generatedBindings != expectedBindings { + t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedBindings, generatedBindings) + } +} diff --git a/v2/internal/binding/binding_test/binding_deepelements_test.go b/v2/internal/binding/binding_test/binding_deepelements_test.go new file mode 100644 index 000000000..034687474 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_deepelements_test.go @@ -0,0 +1,126 @@ +package binding_test + +// Issues 2303, 3442, 3709 + +type DeepMessage struct { + Msg string +} + +type DeepElements struct { + Single []int + Double [][]string + FourDouble [4][]float64 + DoubleFour [][4]int64 + Triple [][][]int + + SingleMap map[string]int + SliceMap map[string][]int + DoubleSliceMap map[string][][]int + + ArrayMap map[string][4]int + DoubleArrayMap1 map[string][4][]int + DoubleArrayMap2 map[string][][4]int + DoubleArrayMap3 map[string][4][4]int + + OneStructs []*DeepMessage + TwoStructs [3][]*DeepMessage + ThreeStructs [][][]DeepMessage + MapStructs map[string][]*DeepMessage + MapTwoStructs map[string][4][]DeepMessage + MapThreeStructs map[string][][7][]*DeepMessage +} + +func (x DeepElements) Get() DeepElements { + return x +} + +var DeepElementsTest = BindingTest{ + name: "DeepElements", + structs: []interface{}{ + &DeepElements{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class DeepMessage { + Msg: string; + + static createFrom(source: any = {}) { + return new DeepMessage(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Msg = source["Msg"]; + } + } + export class DeepElements { + Single: number[]; + Double: string[][]; + FourDouble: number[][]; + DoubleFour: number[][]; + Triple: number[][][]; + SingleMap: Record; + SliceMap: Record>; + DoubleSliceMap: Record>>; + ArrayMap: Record>; + DoubleArrayMap1: Record>>; + DoubleArrayMap2: Record>>; + DoubleArrayMap3: Record>>; + OneStructs: DeepMessage[]; + TwoStructs: DeepMessage[][]; + ThreeStructs: DeepMessage[][][]; + MapStructs: Record>; + MapTwoStructs: Record>>; + MapThreeStructs: Record>>>; + + static createFrom(source: any = {}) { + return new DeepElements(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Single = source["Single"]; + this.Double = source["Double"]; + this.FourDouble = source["FourDouble"]; + this.DoubleFour = source["DoubleFour"]; + this.Triple = source["Triple"]; + this.SingleMap = source["SingleMap"]; + this.SliceMap = source["SliceMap"]; + this.DoubleSliceMap = source["DoubleSliceMap"]; + this.ArrayMap = source["ArrayMap"]; + this.DoubleArrayMap1 = source["DoubleArrayMap1"]; + this.DoubleArrayMap2 = source["DoubleArrayMap2"]; + this.DoubleArrayMap3 = source["DoubleArrayMap3"]; + this.OneStructs = this.convertValues(source["OneStructs"], DeepMessage); + this.TwoStructs = this.convertValues(source["TwoStructs"], DeepMessage); + this.ThreeStructs = this.convertValues(source["ThreeStructs"], DeepMessage); + this.MapStructs = this.convertValues(source["MapStructs"], Array, true); + this.MapTwoStructs = this.convertValues(source["MapTwoStructs"], Array>, true); + this.MapThreeStructs = this.convertValues(source["MapThreeStructs"], Array>>, true); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_emptystruct_test.go b/v2/internal/binding/binding_test/binding_emptystruct_test.go new file mode 100644 index 000000000..ffb85e865 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_emptystruct_test.go @@ -0,0 +1,53 @@ +package binding_test + +type EmptyStruct struct { + Empty struct{} `json:"empty"` +} + +func (s EmptyStruct) Get() EmptyStruct { + return s +} + +var EmptyStructTest = BindingTest{ + name: "EmptyStruct", + structs: []interface{}{ + &EmptyStruct{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class EmptyStruct { + // Go type: struct {} + + empty: any; + + static createFrom(source: any = {}) { + return new EmptyStruct(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.empty = this.convertValues(source["empty"], Object); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_enum_ordering_test.go b/v2/internal/binding/binding_test/binding_enum_ordering_test.go new file mode 100644 index 000000000..0939535ec --- /dev/null +++ b/v2/internal/binding/binding_test/binding_enum_ordering_test.go @@ -0,0 +1,271 @@ +package binding_test + +// Test for PR #4664: Fix generated enums ordering +// This test ensures that enum output is deterministic regardless of map iteration order + +// ZFirstEnum - named with Z prefix to test alphabetical sorting +type ZFirstEnum int + +const ( + ZFirstEnumValue1 ZFirstEnum = iota + ZFirstEnumValue2 +) + +var AllZFirstEnumValues = []struct { + Value ZFirstEnum + TSName string +}{ + {ZFirstEnumValue1, "ZValue1"}, + {ZFirstEnumValue2, "ZValue2"}, +} + +// ASecondEnum - named with A prefix to test alphabetical sorting +type ASecondEnum int + +const ( + ASecondEnumValue1 ASecondEnum = iota + ASecondEnumValue2 +) + +var AllASecondEnumValues = []struct { + Value ASecondEnum + TSName string +}{ + {ASecondEnumValue1, "AValue1"}, + {ASecondEnumValue2, "AValue2"}, +} + +// MMiddleEnum - named with M prefix to test alphabetical sorting +type MMiddleEnum int + +const ( + MMiddleEnumValue1 MMiddleEnum = iota + MMiddleEnumValue2 +) + +var AllMMiddleEnumValues = []struct { + Value MMiddleEnum + TSName string +}{ + {MMiddleEnumValue1, "MValue1"}, + {MMiddleEnumValue2, "MValue2"}, +} + +type EntityWithMultipleEnums struct { + Name string `json:"name"` + EnumZ ZFirstEnum `json:"enumZ"` + EnumA ASecondEnum `json:"enumA"` + EnumM MMiddleEnum `json:"enumM"` +} + +func (e EntityWithMultipleEnums) Get() EntityWithMultipleEnums { + return e +} + +// EnumOrderingTest tests that multiple enums in the same package are output +// in alphabetical order by enum name. Before PR #4664, the order was +// non-deterministic due to Go map iteration order. +var EnumOrderingTest = BindingTest{ + name: "EnumOrderingTest", + structs: []interface{}{ + &EntityWithMultipleEnums{}, + }, + enums: []interface{}{ + // Intentionally add enums in non-alphabetical order + AllZFirstEnumValues, + AllASecondEnumValues, + AllMMiddleEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enums in alphabetical order: ASecondEnum, MMiddleEnum, ZFirstEnum + want: `export namespace binding_test { + + export enum ASecondEnum { + AValue1 = 0, + AValue2 = 1, + } + export enum MMiddleEnum { + MValue1 = 0, + MValue2 = 1, + } + export enum ZFirstEnum { + ZValue1 = 0, + ZValue2 = 1, + } + export class EntityWithMultipleEnums { + name: string; + enumZ: ZFirstEnum; + enumA: ASecondEnum; + enumM: MMiddleEnum; + + static createFrom(source: any = {}) { + return new EntityWithMultipleEnums(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enumZ = source["enumZ"]; + this.enumA = source["enumA"]; + this.enumM = source["enumM"]; + } + } + +} +`, +} + +// EnumElementOrderingEnum tests sorting of enum elements by TSName +type EnumElementOrderingEnum string + +const ( + EnumElementZ EnumElementOrderingEnum = "z_value" + EnumElementA EnumElementOrderingEnum = "a_value" + EnumElementM EnumElementOrderingEnum = "m_value" +) + +// AllEnumElementOrderingValues intentionally lists values out of alphabetical order +// to test that AddEnum sorts them +var AllEnumElementOrderingValues = []struct { + Value EnumElementOrderingEnum + TSName string +}{ + {EnumElementZ, "Zebra"}, + {EnumElementA, "Apple"}, + {EnumElementM, "Mango"}, +} + +type EntityWithUnorderedEnumElements struct { + Name string `json:"name"` + Enum EnumElementOrderingEnum `json:"enum"` +} + +func (e EntityWithUnorderedEnumElements) Get() EntityWithUnorderedEnumElements { + return e +} + +// EnumElementOrderingTest tests that enum elements are sorted alphabetically +// by their TSName within an enum. Before PR #4664, elements appeared in the +// order they were added, which could be arbitrary. +var EnumElementOrderingTest = BindingTest{ + name: "EnumElementOrderingTest", + structs: []interface{}{ + &EntityWithUnorderedEnumElements{}, + }, + enums: []interface{}{ + AllEnumElementOrderingValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum EnumElementOrderingEnum { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithUnorderedEnumElements { + name: string; + enum: EnumElementOrderingEnum; + + static createFrom(source: any = {}) { + return new EntityWithUnorderedEnumElements(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} + +// TSNameEnumElementOrdering tests sorting with TSName() method enum +type TSNameEnumElementOrdering string + +const ( + TSNameEnumZ TSNameEnumElementOrdering = "z_value" + TSNameEnumA TSNameEnumElementOrdering = "a_value" + TSNameEnumM TSNameEnumElementOrdering = "m_value" +) + +func (v TSNameEnumElementOrdering) TSName() string { + switch v { + case TSNameEnumZ: + return "Zebra" + case TSNameEnumA: + return "Apple" + case TSNameEnumM: + return "Mango" + default: + return "Unknown" + } +} + +// AllTSNameEnumValues intentionally out of order +var AllTSNameEnumValues = []TSNameEnumElementOrdering{TSNameEnumZ, TSNameEnumA, TSNameEnumM} + +type EntityWithTSNameEnumOrdering struct { + Name string `json:"name"` + Enum TSNameEnumElementOrdering `json:"enum"` +} + +func (e EntityWithTSNameEnumOrdering) Get() EntityWithTSNameEnumOrdering { + return e +} + +// TSNameEnumElementOrderingTest tests that enums using TSName() method +// also have their elements sorted alphabetically by the TSName. +var TSNameEnumElementOrderingTest = BindingTest{ + name: "TSNameEnumElementOrderingTest", + structs: []interface{}{ + &EntityWithTSNameEnumOrdering{}, + }, + enums: []interface{}{ + AllTSNameEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum TSNameEnumElementOrdering { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithTSNameEnumOrdering { + name: string; + enum: TSNameEnumElementOrdering; + + static createFrom(source: any = {}) { + return new EntityWithTSNameEnumOrdering(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_escapedname_test.go b/v2/internal/binding/binding_test/binding_escapedname_test.go new file mode 100644 index 000000000..1bf6ce3ad --- /dev/null +++ b/v2/internal/binding/binding_test/binding_escapedname_test.go @@ -0,0 +1,32 @@ +package binding_test + +type EscapedName struct { + Name string `json:"n.a.m.e"` +} + +func (s EscapedName) Get() EscapedName { + return s +} + +var EscapedNameTest = BindingTest{ + name: "EscapedName", + structs: []interface{}{ + &EscapedName{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class EscapedName { + "n.a.m.e": string; + static createFrom(source: any = {}) { + return new EscapedName(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this["n.a.m.e"] = source["n.a.m.e"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_generics_test.go b/v2/internal/binding/binding_test/binding_generics_test.go new file mode 100644 index 000000000..920bd2a7a --- /dev/null +++ b/v2/internal/binding/binding_test/binding_generics_test.go @@ -0,0 +1,154 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/float_package" + +// Issues 3900, 3371, 2323 (no TS generics though) + +type ListData[T interface{}] struct { + Total int64 `json:"Total"` + TotalPage int64 `json:"TotalPage"` + PageNum int `json:"PageNum"` + List []T `json:"List,omitempty"` +} + +func (x ListData[T]) Get() ListData[T] { + return x +} + +var Generics1Test = BindingTest{ + name: "Generics1", + structs: []interface{}{ + &ListData[string]{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class ListData_string_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: string[]; + + static createFrom(source: any = {}) { + return new ListData_string_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = source["List"]; + } + } + + } +`, +} + +var Generics2Test = BindingTest{ + name: "Generics2", + structs: []interface{}{ + &ListData[float_package.SomeStruct]{}, + &ListData[*float_package.SomeStruct]{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class ListData__github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: float_package.SomeStruct[]; + + static createFrom(source: any = {}) { + return new ListData__github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = this.convertValues(source["List"], float_package.SomeStruct); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class ListData_github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: float_package.SomeStruct[]; + + static createFrom(source: any = {}) { + return new ListData_github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = this.convertValues(source["List"], float_package.SomeStruct); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } + + export namespace float_package { + + export class SomeStruct { + string: string; + + static createFrom(source: any = {}) { + return new SomeStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.string = source["string"]; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_ignored_test.go b/v2/internal/binding/binding_test/binding_ignored_test.go new file mode 100644 index 000000000..aeb6a9c3f --- /dev/null +++ b/v2/internal/binding/binding_test/binding_ignored_test.go @@ -0,0 +1,47 @@ +package binding_test + +import ( + "unsafe" +) + +// Issues 3755, 3809 + +type Ignored struct { + Valid bool + Total func() int `json:"Total"` + UnsafeP unsafe.Pointer + Complex64 complex64 `json:"Complex"` + Complex128 complex128 + StringChan chan string +} + +func (x Ignored) Get() Ignored { + return x +} + +var IgnoredTest = BindingTest{ + name: "Ignored", + structs: []interface{}{ + &Ignored{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class Ignored { + Valid: boolean; + + static createFrom(source: any = {}) { + return new Ignored(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Valid = source["Valid"]; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedenum_test.go b/v2/internal/binding/binding_test/binding_importedenum_test.go new file mode 100644 index 000000000..5b5b4419e --- /dev/null +++ b/v2/internal/binding/binding_test/binding_importedenum_test.go @@ -0,0 +1,50 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" + +type ImportedEnumStruct struct { + EnumValue binding_test_import.ImportedEnum `json:"EnumValue"` +} + +func (s ImportedEnumStruct) Get() ImportedEnumStruct { + return s +} + +var ImportedEnumTest = BindingTest{ + name: "ImportedEnum", + structs: []interface{}{ + &ImportedEnumStruct{}, + }, + enums: []interface{}{ + binding_test_import.AllImportedEnumValues, + }, + exemptions: nil, + shouldError: false, + want: `export namespace binding_test { + + export class ImportedEnumStruct { + EnumValue: binding_test_import.ImportedEnum; + + static createFrom(source: any = {}) { + return new ImportedEnumStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.EnumValue = source["EnumValue"]; + } + } + + } + + export namespace binding_test_import { + + export enum ImportedEnum { + Value1 = "value1", + Value2 = "value2", + Value3 = "value3", + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedmap_test.go b/v2/internal/binding/binding_test/binding_importedmap_test.go new file mode 100644 index 000000000..4a4b2996c --- /dev/null +++ b/v2/internal/binding/binding_test/binding_importedmap_test.go @@ -0,0 +1,94 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" + +type ImportedMap struct { + AMapWrapperContainer binding_test_import.AMapWrapper `json:"AMapWrapperContainer"` +} + +func (s ImportedMap) Get() ImportedMap { + return s +} + +var ImportedMapTest = BindingTest{ + name: "ImportedMap", + structs: []interface{}{ + &ImportedMap{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class ImportedMap { + AMapWrapperContainer: binding_test_import.AMapWrapper; + static createFrom(source: any = {}) { + return new ImportedMap(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.AMapWrapperContainer = this.convertValues(source["AMapWrapperContainer"], binding_test_import.AMapWrapper); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_import { + export class AMapWrapper { + AMap: Record; + static createFrom(source: any = {}) { + return new AMapWrapper(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.AMap = this.convertValues(source["AMap"], binding_test_nestedimport.A, true); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_nestedimport { + export class A { + A: string; + static createFrom(source: any = {}) { + return new A(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.A = source["A"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedslice_test.go b/v2/internal/binding/binding_test/binding_importedslice_test.go new file mode 100644 index 000000000..5abf55b43 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_importedslice_test.go @@ -0,0 +1,94 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" + +type ImportedSlice struct { + ASliceWrapperContainer binding_test_import.ASliceWrapper `json:"ASliceWrapperContainer"` +} + +func (s ImportedSlice) Get() ImportedSlice { + return s +} + +var ImportedSliceTest = BindingTest{ + name: "ImportedSlice", + structs: []interface{}{ + &ImportedSlice{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class ImportedSlice { + ASliceWrapperContainer: binding_test_import.ASliceWrapper; + static createFrom(source: any = {}) { + return new ImportedSlice(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ASliceWrapperContainer = this.convertValues(source["ASliceWrapperContainer"], binding_test_import.ASliceWrapper); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_import { + export class ASliceWrapper { + ASlice: binding_test_nestedimport.A[]; + static createFrom(source: any = {}) { + return new ASliceWrapper(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ASlice = this.convertValues(source["ASlice"], binding_test_nestedimport.A); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_nestedimport { + export class A { + A: string; + static createFrom(source: any = {}) { + return new A(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.A = source["A"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedstruct_test.go b/v2/internal/binding/binding_test/binding_importedstruct_test.go new file mode 100644 index 000000000..1e94453c2 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_importedstruct_test.go @@ -0,0 +1,95 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" + +type ImportedStruct struct { + AWrapperContainer binding_test_import.AWrapper `json:"AWrapperContainer"` +} + +func (s ImportedStruct) Get() ImportedStruct { + return s +} + +var ImportedStructTest = BindingTest{ + name: "ImportedStruct", + structs: []interface{}{ + &ImportedStruct{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class ImportedStruct { + AWrapperContainer: binding_test_import.AWrapper; + static createFrom(source: any = {}) { + return new ImportedStruct(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.AWrapperContainer = this.convertValues(source["AWrapperContainer"], binding_test_import.AWrapper); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_import { + export class AWrapper { + AWrapper: binding_test_nestedimport.A; + static createFrom(source: any = {}) { + return new AWrapper(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.AWrapper = this.convertValues(source["AWrapper"], binding_test_nestedimport.A); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} + +export namespace binding_test_nestedimport { + export class A { + A: string; + static createFrom(source: any = {}) { + return new A(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.A = source["A"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_multiplestructs_test.go b/v2/internal/binding/binding_test/binding_multiplestructs_test.go new file mode 100644 index 000000000..144867813 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_multiplestructs_test.go @@ -0,0 +1,88 @@ +package binding_test + +type Multistruct1 struct { + Name string `json:"name"` +} + +func (s Multistruct1) Get() Multistruct1 { + return s +} + +type Multistruct2 struct { + Name string `json:"name"` +} + +func (s Multistruct2) Get() Multistruct2 { + return s +} + +type Multistruct3 struct { + Name string `json:"name"` +} + +func (s Multistruct3) Get() Multistruct3 { + return s +} + +type Multistruct4 struct { + Name string `json:"name"` +} + +func (s Multistruct4) Get() Multistruct4 { + return s +} + +var MultistructTest = BindingTest{ + name: "Multistruct", + structs: []interface{}{ + &Multistruct1{}, + &Multistruct2{}, + &Multistruct3{}, + &Multistruct4{}, + }, + exemptions: nil, + shouldError: false, + want: `export namespace binding_test { + export class Multistruct1 { + name: string; + static createFrom(source: any = {}) { + return new Multistruct1(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } + export class Multistruct2 { + name: string; + static createFrom(source: any = {}) { + return new Multistruct2(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } + export class Multistruct3 { + name: string; + static createFrom(source: any = {}) { + return new Multistruct3(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } + export class Multistruct4 { + name: string; + static createFrom(source: any = {}) { + return new Multistruct4(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_nestedfield_test.go b/v2/internal/binding/binding_test/binding_nestedfield_test.go new file mode 100644 index 000000000..66dd11cbf --- /dev/null +++ b/v2/internal/binding/binding_test/binding_nestedfield_test.go @@ -0,0 +1,63 @@ +package binding_test + +type As struct { + B Bs `json:"b"` +} + +type Bs struct { + Name string `json:"name"` +} + +func (a As) Get() As { + return a +} + +var NestedFieldTest = BindingTest{ + name: "NestedField", + structs: []interface{}{ + &As{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class Bs { + name: string; + static createFrom(source: any = {}) { + return new Bs(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } + export class As { + b: Bs; + static createFrom(source: any = {}) { + return new As(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.b = this.convertValues(source["b"], Bs); + } + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go b/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go new file mode 100644 index 000000000..9efee710f --- /dev/null +++ b/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go @@ -0,0 +1,32 @@ +package binding_test + +type NonStringMapKey struct { + NumberMap map[uint]any `json:"numberMap"` +} + +func (s NonStringMapKey) Get() NonStringMapKey { + return s +} + +var NonStringMapKeyTest = BindingTest{ + name: "NonStringMapKey", + structs: []interface{}{ + &NonStringMapKey{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class NonStringMapKey { + numberMap: Record; + static createFrom(source: any = {}) { + return new NonStringMapKey(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.numberMap = source["numberMap"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_notags_test.go b/v2/internal/binding/binding_test/binding_notags_test.go new file mode 100644 index 000000000..d4d9997e0 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_notags_test.go @@ -0,0 +1,60 @@ +package binding_test + +type NoFieldTags struct { + Name string + Address string + Zip *string + Spouse *NoFieldTags + NoFunc func() string +} + +func (n NoFieldTags) Get() NoFieldTags { + return n +} + +var NoFieldTagsTest = BindingTest{ + name: "NoFieldTags", + structs: []interface{}{ + &NoFieldTags{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class NoFieldTags { + Name: string; + Address: string; + Zip?: string; + Spouse?: NoFieldTags; + static createFrom(source: any = {}) { + return new NoFieldTags(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Name = source["Name"]; + this.Address = source["Address"]; + this.Zip = source["Zip"]; + this.Spouse = this.convertValues(source["Spouse"], NoFieldTags); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_returned_promises_test.go b/v2/internal/binding/binding_test/binding_returned_promises_test.go new file mode 100644 index 000000000..94941d0a3 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_returned_promises_test.go @@ -0,0 +1,81 @@ +package binding_test + +import ( + "io/fs" + "os" + "testing" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/logger" +) + +const expectedPromiseBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {binding_test} from '../models'; + +export function ErrorReturn(arg1:number):Promise; + +export function NoReturn(arg1:string):Promise; + +export function SingleReturn(arg1:any):Promise; + +export function SingleReturnStruct(arg1:any):Promise; + +export function SingleReturnStructPointer(arg1:any):Promise; + +export function SingleReturnStructPointerSlice(arg1:any):Promise>; + +export function SingleReturnStructSlice(arg1:any):Promise>; + +export function SingleReturnWithError(arg1:number):Promise; + +export function TwoReturn(arg1:any):Promise; +` + +type PromisesTest struct{} +type PromisesTestReturnStruct struct{} + +func (h *PromisesTest) NoReturn(_ string) {} +func (h *PromisesTest) ErrorReturn(_ int) error { return nil } +func (h *PromisesTest) SingleReturn(_ interface{}) int { return 0 } +func (h *PromisesTest) SingleReturnStructPointer(_ interface{}) *PromisesTestReturnStruct { + return &PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStruct(_ interface{}) PromisesTestReturnStruct { + return PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStructSlice(_ interface{}) []PromisesTestReturnStruct { + return []PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStructPointerSlice(_ interface{}) []*PromisesTestReturnStruct { + return []*PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnWithError(_ int) (string, error) { return "", nil } +func (h *PromisesTest) TwoReturn(_ interface{}) (string, int) { return "", 0 } + +func TestPromises(t *testing.T) { + // given + generationDir := t.TempDir() + + // setup + testLogger := &logger.Logger{} + b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false, []interface{}{}) + + // then + err := b.GenerateGoBindings(generationDir) + if err != nil { + t.Fatalf("could not generate the Go bindings: %v", err) + } + + // then + rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/PromisesTest.d.ts") + if err != nil { + t.Fatalf("could not read the generated bindings: %v", err) + } + + // then + generatedBindings := string(rawGeneratedBindings) + if generatedBindings != expectedPromiseBindings { + t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedPromiseBindings, generatedBindings) + } +} diff --git a/v2/internal/binding/binding_test/binding_singlefield_test.go b/v2/internal/binding/binding_test/binding_singlefield_test.go new file mode 100644 index 000000000..2919ab29e --- /dev/null +++ b/v2/internal/binding/binding_test/binding_singlefield_test.go @@ -0,0 +1,32 @@ +package binding_test + +type SingleField struct { + Name string `json:"name"` +} + +func (s SingleField) Get() SingleField { + return s +} + +var SingleFieldTest = BindingTest{ + name: "SingleField", + structs: []interface{}{ + &SingleField{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class SingleField { + name: string; + static createFrom(source: any = {}) { + return new SingleField(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_structwithoutfields_test.go b/v2/internal/binding/binding_test/binding_structwithoutfields_test.go new file mode 100644 index 000000000..4b2289b98 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_structwithoutfields_test.go @@ -0,0 +1,34 @@ +package binding_test + +type WithoutFields struct { +} + +func (s WithoutFields) Get() WithoutFields { + return s +} + +var WithoutFieldsTest = BindingTest{ + name: "StructWithoutFields", + structs: []interface{}{ + &WithoutFields{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class WithoutFields { + + + static createFrom(source: any = {}) { + return new WithoutFields(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + + } + } + +}`, +} diff --git a/v2/internal/binding/binding_test/binding_test.go b/v2/internal/binding/binding_test/binding_test.go new file mode 100644 index 000000000..41f0618ce --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test.go @@ -0,0 +1,85 @@ +package binding_test + +import ( + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/logger" +) + +type BindingTest struct { + name string + structs []interface{} + enums []interface{} + exemptions []interface{} + want string + shouldError bool + TsGenerationOptionsTest +} + +type TsGenerationOptionsTest struct { + TsPrefix string + TsSuffix string + TsOutputType string +} + +func TestBindings_GenerateModels(t *testing.T) { + + tests := []BindingTest{ + EscapedNameTest, + ImportedStructTest, + ImportedSliceTest, + ImportedMapTest, + ImportedEnumTest, + NestedFieldTest, + NonStringMapKeyTest, + SingleFieldTest, + MultistructTest, + EmptyStructTest, + GeneratedJsEntityTest, + GeneratedJsEntityWithIntEnumTest, + GeneratedJsEntityWithStringEnumTest, + GeneratedJsEntityWithEnumTsName, + GeneratedJsEntityWithNestedStructInterfacesTest, + AnonymousSubStructTest, + AnonymousSubStructMultiLevelTest, + GeneratedJsEntityWithNestedStructTest, + EntityWithDiffNamespacesTest, + SpecialCharacterFieldTest, + WithoutFieldsTest, + NoFieldTagsTest, + Generics1Test, + Generics2Test, + IgnoredTest, + DeepElementsTest, + // PR #4664: Enum ordering tests + EnumOrderingTest, + EnumElementOrderingTest, + TSNameEnumElementOrderingTest, + } + + testLogger := &logger.Logger{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false, tt.enums) + for _, s := range tt.structs { + err := b.Add(s) + require.NoError(t, err) + } + b.SetTsPrefix(tt.TsPrefix) + b.SetTsSuffix(tt.TsSuffix) + b.SetOutputType(tt.TsOutputType) + got, err := b.GenerateModels() + if (err != nil) != tt.shouldError { + t.Errorf("GenerateModels() error = %v, shouldError %v", err, tt.shouldError) + return + } + if !reflect.DeepEqual(strings.Fields(string(got)), strings.Fields(tt.want)) { + t.Errorf("GenerateModels() got = %v, want %v", string(got), tt.want) + } + }) + } +} diff --git a/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go b/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go new file mode 100644 index 000000000..e7080c694 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go @@ -0,0 +1,32 @@ +package binding_test_import + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/binding_test_nestedimport" + +type AWrapper struct { + AWrapper binding_test_nestedimport.A `json:"AWrapper"` +} + +type ASliceWrapper struct { + ASlice []binding_test_nestedimport.A `json:"ASlice"` +} + +type AMapWrapper struct { + AMap map[string]binding_test_nestedimport.A `json:"AMap"` +} + +type ImportedEnum string + +const ( + ImportedEnumValue1 ImportedEnum = "value1" + ImportedEnumValue2 ImportedEnum = "value2" + ImportedEnumValue3 ImportedEnum = "value3" +) + +var AllImportedEnumValues = []struct { + Value ImportedEnum + TSName string +}{ + {ImportedEnumValue1, "Value1"}, + {ImportedEnumValue2, "Value2"}, + {ImportedEnumValue3, "Value3"}, +} diff --git a/v2/internal/binding/binding_test/binding_test_import/binding_test_nestedimport/binding_nestedimport.go b/v2/internal/binding/binding_test/binding_test_import/binding_test_nestedimport/binding_nestedimport.go new file mode 100644 index 000000000..31c70ad3f --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/binding_test_nestedimport/binding_nestedimport.go @@ -0,0 +1,5 @@ +package binding_test_nestedimport + +type A struct { + A string `json:"A"` +} diff --git a/v2/internal/binding/binding_test/binding_test_import/float_package/type.go b/v2/internal/binding/binding_test/binding_test_import/float_package/type.go new file mode 100644 index 000000000..66a20304b --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/float_package/type.go @@ -0,0 +1,5 @@ +package float_package + +type SomeStruct struct { + Name string `json:"string"` +} diff --git a/v2/internal/binding/binding_test/binding_test_import/int_package/type.go b/v2/internal/binding/binding_test/binding_test_import/int_package/type.go new file mode 100644 index 000000000..bff29946a --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/int_package/type.go @@ -0,0 +1,5 @@ +package int_package + +type SomeStruct struct { + Name string `json:"string"` +} diff --git a/v2/internal/binding/binding_test/binding_test_import/map_package/type.go b/v2/internal/binding/binding_test/binding_test_import/map_package/type.go new file mode 100644 index 000000000..34303757c --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/map_package/type.go @@ -0,0 +1,5 @@ +package map_package + +type SomeStruct struct { + Name string `json:"string"` +} diff --git a/v2/internal/binding/binding_test/binding_test_import/uint_package/type.go b/v2/internal/binding/binding_test/binding_test_import/uint_package/type.go new file mode 100644 index 000000000..dd55675f6 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_test_import/uint_package/type.go @@ -0,0 +1,5 @@ +package uint_package + +type SomeStruct struct { + Name string `json:"string"` +} diff --git a/v2/internal/binding/binding_test/binding_tsgeneration_test.go b/v2/internal/binding/binding_test/binding_tsgeneration_test.go new file mode 100644 index 000000000..850bc778a --- /dev/null +++ b/v2/internal/binding/binding_test/binding_tsgeneration_test.go @@ -0,0 +1,509 @@ +package binding_test + +import ( + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" +) + +type GeneratedJsEntity struct { + Name string `json:"name"` +} + +func (s GeneratedJsEntity) Get() GeneratedJsEntity { + return s +} + +var GeneratedJsEntityTest = BindingTest{ + name: "GeneratedJsEntityTest", + structs: []interface{}{ + &GeneratedJsEntity{}, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: ` +export namespace binding_test { + + export class MY_PREFIX_GeneratedJsEntity_MY_SUFFIX { + name: string; + + static createFrom(source: any = {}) { + return new MY_PREFIX_GeneratedJsEntity_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + } + } + +} + +`, +} + +type ParentEntity struct { + Name string `json:"name"` + Ref ChildEntity `json:"ref"` + ParentProp string `json:"parentProp"` +} + +func (p ParentEntity) Get() ParentEntity { + return p +} + +type ChildEntity struct { + Name string `json:"name"` + ChildProp int `json:"childProp"` +} + +var GeneratedJsEntityWithNestedStructTest = BindingTest{ + name: "GeneratedJsEntityWithNestedStructTest", + structs: []interface{}{ + &ParentEntity{}, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: ` +export namespace binding_test { + + export class MY_PREFIX_ChildEntity_MY_SUFFIX { + name: string; + childProp: number; + + static createFrom(source: any = {}) { + return new MY_PREFIX_ChildEntity_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.childProp = source["childProp"]; + } + } + export class MY_PREFIX_ParentEntity_MY_SUFFIX { + name: string; + ref: MY_PREFIX_ChildEntity_MY_SUFFIX; + parentProp: string; + + static createFrom(source: any = {}) { + return new MY_PREFIX_ParentEntity_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.ref = this.convertValues(source["ref"], MY_PREFIX_ChildEntity_MY_SUFFIX); + this.parentProp = source["parentProp"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } +`, +} + +type ParentPackageEntity struct { + Name string `json:"name"` + Ref ChildPackageEntity `json:"ref"` +} + +func (p ParentPackageEntity) Get() ParentPackageEntity { + return p +} + +type ChildPackageEntity struct { + Name string `json:"name"` + ImportedPackage binding_test_import.AWrapper `json:"importedPackage"` +} + +var EntityWithDiffNamespacesTest = BindingTest{ + name: "EntityWithDiffNamespaces ", + structs: []interface{}{ + &ParentPackageEntity{}, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: ` +export namespace binding_test { + + export class MY_PREFIX_ChildPackageEntity_MY_SUFFIX { + name: string; + importedPackage: binding_test_import.MY_PREFIX_AWrapper_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_ChildPackageEntity_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.importedPackage = this.convertValues(source["importedPackage"], binding_test_import.MY_PREFIX_AWrapper_MY_SUFFIX); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class MY_PREFIX_ParentPackageEntity_MY_SUFFIX { + name: string; + ref: MY_PREFIX_ChildPackageEntity_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_ParentPackageEntity_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.ref = this.convertValues(source["ref"], MY_PREFIX_ChildPackageEntity_MY_SUFFIX); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } + + export namespace binding_test_import { + + export class MY_PREFIX_AWrapper_MY_SUFFIX { + AWrapper: binding_test_nestedimport.MY_PREFIX_A_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_AWrapper_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.AWrapper = this.convertValues(source["AWrapper"], binding_test_nestedimport.MY_PREFIX_A_MY_SUFFIX); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } + + export namespace binding_test_nestedimport { + + export class MY_PREFIX_A_MY_SUFFIX { + A: string; + + static createFrom(source: any = {}) { + return new MY_PREFIX_A_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.A = source["A"]; + } + } + + } + +`, +} + +type IntEnum int + +const ( + IntEnumValue1 IntEnum = iota + IntEnumValue2 + IntEnumValue3 +) + +var AllIntEnumValues = []struct { + Value IntEnum + TSName string +}{ + {IntEnumValue1, "Value1"}, + {IntEnumValue2, "Value2"}, + {IntEnumValue3, "Value3"}, +} + +type EntityWithIntEnum struct { + Name string `json:"name"` + Enum IntEnum `json:"enum"` +} + +func (e EntityWithIntEnum) Get() EntityWithIntEnum { + return e +} + +var GeneratedJsEntityWithIntEnumTest = BindingTest{ + name: "GeneratedJsEntityWithIntEnumTest", + structs: []interface{}{ + &EntityWithIntEnum{}, + }, + enums: []interface{}{ + AllIntEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_IntEnum_MY_SUFFIX { + Value1 = 0, + Value2 = 1, + Value3 = 2, + } + export class MY_PREFIX_EntityWithIntEnum_MY_SUFFIX { + name: string; + enum: MY_PREFIX_IntEnum_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithIntEnum_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +type StringEnum string + +const ( + StringEnumValue1 StringEnum = "value1" + StringEnumValue2 StringEnum = "value2" + StringEnumValue3 StringEnum = "value3" +) + +var AllStringEnumValues = []struct { + Value StringEnum + TSName string +}{ + {StringEnumValue1, "Value1"}, + {StringEnumValue2, "Value2"}, + {StringEnumValue3, "Value3"}, +} + +type EntityWithStringEnum struct { + Name string `json:"name"` + Enum StringEnum `json:"enum"` +} + +func (e EntityWithStringEnum) Get() EntityWithStringEnum { + return e +} + +var GeneratedJsEntityWithStringEnumTest = BindingTest{ + name: "GeneratedJsEntityWithStringEnumTest", + structs: []interface{}{ + &EntityWithStringEnum{}, + }, + enums: []interface{}{ + AllStringEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_StringEnum_MY_SUFFIX { + Value1 = "value1", + Value2 = "value2", + Value3 = "value3", + } + export class MY_PREFIX_EntityWithStringEnum_MY_SUFFIX { + name: string; + enum: MY_PREFIX_StringEnum_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithStringEnum_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +type EnumWithTsName string + +const ( + EnumWithTsName1 EnumWithTsName = "value1" + EnumWithTsName2 EnumWithTsName = "value2" + EnumWithTsName3 EnumWithTsName = "value3" +) + +var AllEnumWithTsNameValues = []EnumWithTsName{EnumWithTsName1, EnumWithTsName2, EnumWithTsName3} + +func (v EnumWithTsName) TSName() string { + switch v { + case EnumWithTsName1: + return "TsName1" + case EnumWithTsName2: + return "TsName2" + case EnumWithTsName3: + return "TsName3" + default: + return "???" + } +} + +type EntityWithEnumTsName struct { + Name string `json:"name"` + Enum EnumWithTsName `json:"enum"` +} + +func (e EntityWithEnumTsName) Get() EntityWithEnumTsName { + return e +} + +var GeneratedJsEntityWithEnumTsName = BindingTest{ + name: "GeneratedJsEntityWithEnumTsName", + structs: []interface{}{ + &EntityWithEnumTsName{}, + }, + enums: []interface{}{ + AllEnumWithTsNameValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_EnumWithTsName_MY_SUFFIX { + TsName1 = "value1", + TsName2 = "value2", + TsName3 = "value3", + } + export class MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX { + name: string; + enum: MY_PREFIX_EnumWithTsName_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +var GeneratedJsEntityWithNestedStructInterfacesTest = BindingTest{ + name: "GeneratedJsEntityWithNestedStructInterfacesTest", + structs: []interface{}{ + &ParentEntity{}, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + TsOutputType: "interfaces", + }, + want: `export namespace binding_test { + + export interface MY_PREFIX_ChildEntity_MY_SUFFIX { + name: string; + childProp: number; + } + export interface MY_PREFIX_ParentEntity_MY_SUFFIX { + name: string; + ref: MY_PREFIX_ChildEntity_MY_SUFFIX; + parentProp: string; + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_type_alias_test.go b/v2/internal/binding/binding_test/binding_type_alias_test.go new file mode 100644 index 000000000..90b009c5f --- /dev/null +++ b/v2/internal/binding/binding_test/binding_type_alias_test.go @@ -0,0 +1,64 @@ +package binding_test + +import ( + "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/int_package" + "io/fs" + "os" + "testing" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/logger" +) + +const expectedTypeAliasBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {binding_test} from '../models'; +import {int_package} from '../models'; + +export function Map():Promise>; + +export function MapAlias():Promise; + +export function MapWithImportedStructValue():Promise>; + +export function Slice():Promise>; + +export function SliceImportedStruct():Promise>; +` + +type AliasTest struct{} +type MapAlias map[string]string + +func (h *AliasTest) Map() map[string]string { return nil } +func (h *AliasTest) MapAlias() MapAlias { return nil } +func (h *AliasTest) MapWithImportedStructValue() map[string]int_package.SomeStruct { return nil } +func (h *AliasTest) Slice() []string { return nil } +func (h *AliasTest) SliceImportedStruct() []int_package.SomeStruct { return nil } + +func TestAliases(t *testing.T) { + // given + generationDir := t.TempDir() + + // setup + testLogger := &logger.Logger{} + b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false, []interface{}{}) + + // then + err := b.GenerateGoBindings(generationDir) + if err != nil { + t.Fatalf("could not generate the Go bindings: %v", err) + } + + // then + rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/AliasTest.d.ts") + if err != nil { + t.Fatalf("could not read the generated bindings: %v", err) + } + + // then + generatedBindings := string(rawGeneratedBindings) + if generatedBindings != expectedTypeAliasBindings { + t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedTypeAliasBindings, + generatedBindings) + } +} diff --git a/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go b/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go new file mode 100644 index 000000000..7dbe72350 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go @@ -0,0 +1,32 @@ +package binding_test + +type SpecialCharacterField struct { + ID string `json:"@ID,omitempty"` +} + +func (s SpecialCharacterField) Get() SpecialCharacterField { + return s +} + +var SpecialCharacterFieldTest = BindingTest{ + name: "SpecialCharacterField", + structs: []interface{}{ + &SpecialCharacterField{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class SpecialCharacterField { + "@ID"?: string; + static createFrom(source: any = {}) { + return new SpecialCharacterField(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this["@ID"] = source["@ID"]; + } + } +} +`, +} diff --git a/v2/internal/binding/boundMethod.go b/v2/internal/binding/boundMethod.go new file mode 100644 index 000000000..e697041b0 --- /dev/null +++ b/v2/internal/binding/boundMethod.go @@ -0,0 +1,109 @@ +package binding + +import ( + "encoding/json" + "fmt" + "reflect" +) + +type BoundedMethodPath struct { + Package string + Struct string + Name string +} + +func (p *BoundedMethodPath) FullName() string { + return fmt.Sprintf("%s.%s.%s", p.Package, p.Struct, p.Name) +} + +// BoundMethod defines all the data related to a Go method that is +// bound to the Wails application +type BoundMethod struct { + Path *BoundedMethodPath `json:"path"` + Inputs []*Parameter `json:"inputs,omitempty"` + Outputs []*Parameter `json:"outputs,omitempty"` + Comments string `json:"comments,omitempty"` + Method reflect.Value `json:"-"` +} + +// InputCount returns the number of inputs this bound method has +func (b *BoundMethod) InputCount() int { + return len(b.Inputs) +} + +// OutputCount returns the number of outputs this bound method has +func (b *BoundMethod) OutputCount() int { + return len(b.Outputs) +} + +// ParseArgs method converts the input json into the types expected by the method +func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) { + result := make([]interface{}, b.InputCount()) + if len(args) != b.InputCount() { + return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Path.FullName(), b.InputCount()) + } + for index, arg := range args { + typ := b.Inputs[index].reflectType + inputValue := reflect.New(typ).Interface() + err := json.Unmarshal(arg, inputValue) + if err != nil { + return nil, err + } + if inputValue == nil { + result[index] = reflect.Zero(typ).Interface() + } else { + result[index] = reflect.ValueOf(inputValue).Elem().Interface() + } + } + return result, nil +} + +// Call will attempt to call this bound method with the given args +func (b *BoundMethod) Call(args []interface{}) (interface{}, error) { + // Check inputs + expectedInputLength := len(b.Inputs) + actualInputLength := len(args) + if expectedInputLength != actualInputLength { + return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Path.FullName(), expectedInputLength, actualInputLength) + } + + /** Convert inputs to reflect values **/ + + // Create slice for the input arguments to the method call + callArgs := make([]reflect.Value, expectedInputLength) + + // Iterate over given arguments + for index, arg := range args { + // Save the converted argument + callArgs[index] = reflect.ValueOf(arg) + } + + // Do the call + callResults := b.Method.Call(callArgs) + + //** Check results **// + var returnValue interface{} + var err error + + switch b.OutputCount() { + case 1: + // Loop over results and determine if the result + // is an error or not + for _, result := range callResults { + interfac := result.Interface() + temp, ok := interfac.(error) + if ok { + err = temp + } else { + returnValue = interfac + } + } + case 2: + returnValue = callResults[0].Interface() + if temp, ok := callResults[1].Interface().(error); ok { + err = temp + } + } + + return returnValue, err +} diff --git a/v2/internal/binding/db.go b/v2/internal/binding/db.go new file mode 100644 index 000000000..f7b793839 --- /dev/null +++ b/v2/internal/binding/db.go @@ -0,0 +1,134 @@ +package binding + +import ( + "encoding/json" + "sync" + "unsafe" +) + +// DB is our database of method bindings +type DB struct { + // map[packagename] -> map[structname] -> map[methodname]*method + store map[string]map[string]map[string]*BoundMethod + + // This uses fully qualified method names as a shortcut for store traversal. + // It used for performance gains at runtime + methodMap map[string]*BoundMethod + + // This uses ids to reference bound methods at runtime + obfuscatedMethodArray []*ObfuscatedMethod + + // Lock to ensure sync access to the data + lock sync.RWMutex +} + +type ObfuscatedMethod struct { + method *BoundMethod + methodName string +} + +func newDB() *DB { + return &DB{ + store: make(map[string]map[string]map[string]*BoundMethod), + methodMap: make(map[string]*BoundMethod), + obfuscatedMethodArray: []*ObfuscatedMethod{}, + } +} + +// GetMethodFromStore returns the method for the given package/struct/method names +// nil is returned if any one of those does not exist +func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod { + // Lock the db whilst processing and unlock on return + d.lock.RLock() + defer d.lock.RUnlock() + + structMap, exists := d.store[packageName] + if !exists { + return nil + } + methodMap, exists := structMap[structName] + if !exists { + return nil + } + return methodMap[methodName] +} + +// GetMethod returns the method for the given qualified method name +// qualifiedMethodName is "packagename.structname.methodname" +func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod { + // Lock the db whilst processing and unlock on return + d.lock.RLock() + defer d.lock.RUnlock() + + return d.methodMap[qualifiedMethodName] +} + +// GetObfuscatedMethod returns the method for the given ID +func (d *DB) GetObfuscatedMethod(id int) *BoundMethod { + // Lock the db whilst processing and unlock on return + d.lock.RLock() + defer d.lock.RUnlock() + + if len(d.obfuscatedMethodArray) <= id { + return nil + } + + return d.obfuscatedMethodArray[id].method +} + +// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName +func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) { + // Lock the db whilst processing and unlock on return + d.lock.Lock() + defer d.lock.Unlock() + + // Get the map associated with the package name + structMap, exists := d.store[packageName] + if !exists { + // Create a new map for this packagename + d.store[packageName] = make(map[string]map[string]*BoundMethod) + structMap = d.store[packageName] + } + + // Get the map associated with the struct name + methodMap, exists := structMap[structName] + if !exists { + // Create a new map for this packagename + structMap[structName] = make(map[string]*BoundMethod) + methodMap = structMap[structName] + } + + // Store the method definition + methodMap[methodName] = methodDefinition + + // Store in the methodMap + key := packageName + "." + structName + "." + methodName + d.methodMap[key] = methodDefinition + d.obfuscatedMethodArray = append(d.obfuscatedMethodArray, &ObfuscatedMethod{method: methodDefinition, methodName: key}) +} + +// ToJSON converts the method map to JSON +func (d *DB) ToJSON() (string, error) { + // Lock the db whilst processing and unlock on return + d.lock.RLock() + defer d.lock.RUnlock() + + d.UpdateObfuscatedCallMap() + + bytes, err := json.Marshal(&d.store) + + // Return zero copy string as this string will be read only + result := *(*string)(unsafe.Pointer(&bytes)) + return result, err +} + +// UpdateObfuscatedCallMap sets up the secure call mappings +func (d *DB) UpdateObfuscatedCallMap() map[string]int { + mappings := make(map[string]int) + + for id, k := range d.obfuscatedMethodArray { + mappings[k.methodName] = id + } + + return mappings +} diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go new file mode 100644 index 000000000..77edc983d --- /dev/null +++ b/v2/internal/binding/generate.go @@ -0,0 +1,248 @@ +package binding + +import ( + "bytes" + _ "embed" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/wailsapp/wails/v2/internal/fs" + + "github.com/leaanthony/slicer" +) + +var ( + mapRegex *regexp.Regexp + keyPackageIndex int + keyTypeIndex int + valueArrayIndex int + valuePackageIndex int + valueTypeIndex int +) + +func init() { + mapRegex = regexp.MustCompile(`(?:map\[(?:(?P\w+)\.)?(?P\w+)])?(?P\[])?(?:\*?(?P\w+)\.)?(?P.+)`) + keyPackageIndex = mapRegex.SubexpIndex("keyPackage") + keyTypeIndex = mapRegex.SubexpIndex("keyType") + valueArrayIndex = mapRegex.SubexpIndex("valueArray") + valuePackageIndex = mapRegex.SubexpIndex("valuePackage") + valueTypeIndex = mapRegex.SubexpIndex("valueType") +} + +func (b *Bindings) GenerateGoBindings(baseDir string) error { + store := b.db.store + var obfuscatedBindings map[string]int + if b.obfuscate { + obfuscatedBindings = b.db.UpdateObfuscatedCallMap() + } + for packageName, structs := range store { + packageDir := filepath.Join(baseDir, packageName) + err := fs.Mkdir(packageDir) + if err != nil { + return err + } + for structName, methods := range structs { + var jsoutput bytes.Buffer + jsoutput.WriteString(`// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +`) + var tsBody bytes.Buffer + var tsContent bytes.Buffer + tsContent.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +`) + // Sort the method names alphabetically + methodNames := make([]string, 0, len(methods)) + for methodName := range methods { + methodNames = append(methodNames, methodName) + } + sort.Strings(methodNames) + + var importNamespaces slicer.StringSlicer + for _, methodName := range methodNames { + // Get the method details + methodDetails := methods[methodName] + + // Generate JS + var args slicer.StringSlicer + for count := range methodDetails.Inputs { + arg := fmt.Sprintf("arg%d", count+1) + args.Add(arg) + } + argsString := args.Join(", ") + jsoutput.WriteString(fmt.Sprintf("\nexport function %s(%s) {", methodName, argsString)) + jsoutput.WriteString("\n") + if b.obfuscate { + id := obfuscatedBindings[strings.Join([]string{packageName, structName, methodName}, ".")] + jsoutput.WriteString(fmt.Sprintf(" return ObfuscatedCall(%d, [%s]);", id, argsString)) + } else { + jsoutput.WriteString(fmt.Sprintf(" return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString)) + } + jsoutput.WriteString("\n}\n") + + // Generate TS + tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName)) + + args.Clear() + for count, input := range methodDetails.Inputs { + arg := fmt.Sprintf("arg%d", count+1) + entityName := entityFullReturnType(input.TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces) + args.Add(arg + ":" + goTypeToTypescriptType(entityName, &importNamespaces)) + } + tsBody.WriteString(args.Join(",") + "):") + // now build Typescript return types + // If there is no return value or only returning error, TS returns Promise + // If returning single value, TS returns Promise + // If returning single value or error, TS returns Promise + // If returning two values, TS returns Promise + // Otherwise, TS returns Promise (instead of throwing Go error?) + var returnType string + if methodDetails.OutputCount() == 0 { + returnType = "Promise" + } else if methodDetails.OutputCount() == 1 && methodDetails.Outputs[0].TypeName == "error" { + returnType = "Promise" + } else { + outputTypeName := entityFullReturnType(methodDetails.Outputs[0].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces) + firstType := goTypeToTypescriptType(outputTypeName, &importNamespaces) + returnType = "Promise<" + firstType + if methodDetails.OutputCount() == 2 && methodDetails.Outputs[1].TypeName != "error" { + outputTypeName = entityFullReturnType(methodDetails.Outputs[1].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces) + secondType := goTypeToTypescriptType(outputTypeName, &importNamespaces) + returnType += "|" + secondType + } + returnType += ">" + } + tsBody.WriteString(returnType + ";\n") + } + + importNamespaces.Deduplicate() + importNamespaces.Each(func(namespace string) { + tsContent.WriteString("import {" + namespace + "} from '../models';\n") + }) + tsContent.WriteString(tsBody.String()) + + jsfilename := filepath.Join(packageDir, structName+".js") + err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0o755) + if err != nil { + return err + } + tsfilename := filepath.Join(packageDir, structName+".d.ts") + err = os.WriteFile(tsfilename, tsContent.Bytes(), 0o755) + if err != nil { + return err + } + } + } + err := b.WriteModels(baseDir) + if err != nil { + return err + } + return nil +} + +func fullyQualifiedName(packageName string, typeName string) string { + if len(packageName) > 0 { + return packageName + "." + typeName + } + + switch true { + case len(typeName) == 0: + return "" + case typeName == "interface{}" || typeName == "interface {}": + return "any" + case typeName == "string": + return "string" + case typeName == "error": + return "Error" + case + strings.HasPrefix(typeName, "int"), + strings.HasPrefix(typeName, "uint"), + strings.HasPrefix(typeName, "float"): + return "number" + case typeName == "bool": + return "boolean" + default: + return "any" + } +} + +var ( + jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`) +) + +func arrayifyValue(valueArray string, valueType string) string { + valueType = strings.ReplaceAll(valueType, "*", "") + gidx := strings.IndexRune(valueType, '[') + if gidx > 0 { // its a generic type + rem := strings.SplitN(valueType, "[", 2) + valueType = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_") + } + + if len(valueArray) == 0 { + return valueType + } + + return "Array<" + valueType + ">" +} + +func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string { + matches := mapRegex.FindStringSubmatch(input) + keyPackage := matches[keyPackageIndex] + keyType := matches[keyTypeIndex] + valueArray := matches[valueArrayIndex] + valuePackage := matches[valuePackageIndex] + valueType := matches[valueTypeIndex] + // fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n", + // input, + // keyPackage, + // keyType, + // valueArray, + // valuePackage, + // valueType) + + // byte array is special case + if valueArray == "[]" && valueType == "byte" { + return "string" + } + + // if any packages, make sure they're saved + if len(keyPackage) > 0 { + importNamespaces.Add(keyPackage) + } + + if len(valuePackage) > 0 { + importNamespaces.Add(valuePackage) + } + + key := fullyQualifiedName(keyPackage, keyType) + var value string + if strings.HasPrefix(valueType, "map") { + value = goTypeToJSDocType(valueType, importNamespaces) + } else { + value = fullyQualifiedName(valuePackage, valueType) + } + + if len(key) > 0 { + return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value)) + } + + return arrayifyValue(valueArray, value) +} + +func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string { + return goTypeToJSDocType(input, importNamespaces) +} + +func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string { + if strings.ContainsRune(input, '.') { + nameSpace, returnType := getSplitReturn(input) + return nameSpace + "." + prefix + returnType + suffix + } + + return input +} diff --git a/v2/internal/binding/generate_test.go b/v2/internal/binding/generate_test.go new file mode 100644 index 000000000..26d7c70df --- /dev/null +++ b/v2/internal/binding/generate_test.go @@ -0,0 +1,150 @@ +package binding + +import ( + "testing" + + "github.com/leaanthony/slicer" + "github.com/stretchr/testify/assert" + "github.com/wailsapp/wails/v2/internal/logger" +) + +type BindForTest struct { +} + +func (b *BindForTest) GetA() A { + return A{} +} + +type A struct { + B B `json:"B"` +} + +type B struct { + Name string `json:"name"` +} + +func TestNestedStruct(t *testing.T) { + bind := &BindForTest{} + testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false, []interface{}{}) + + namesStrSlicer := testBindings.getAllStructNames() + names := []string{} + namesStrSlicer.Each(func(s string) { + names = append(names, s) + }) + assert.Contains(t, names, "binding.A") + assert.Contains(t, names, "binding.B") +} + +func Test_goTypeToJSDocType(t *testing.T) { + + tests := []struct { + name string + input string + want string + }{ + { + name: "string", + input: "string", + want: "string", + }, + { + name: "error", + input: "error", + want: "Error", + }, + { + name: "int", + input: "int", + want: "number", + }, + { + name: "int32", + input: "int32", + want: "number", + }, + { + name: "uint", + input: "uint", + want: "number", + }, + { + name: "uint32", + input: "uint32", + want: "number", + }, + { + name: "float32", + input: "float32", + want: "number", + }, + { + name: "float64", + input: "float64", + want: "number", + }, + { + name: "bool", + input: "bool", + want: "boolean", + }, + { + name: "interface{}", + input: "interface{}", + want: "any", + }, + { + name: "[]byte", + input: "[]byte", + want: "string", + }, + { + name: "[]int", + input: "[]int", + want: "Array", + }, + { + name: "[]bool", + input: "[]bool", + want: "Array", + }, + { + name: "anything else", + input: "foo", + want: "any", + }, + { + name: "map", + input: "map[string]float64", + want: "Record", + }, + { + name: "map", + input: "map[string]map[string]float64", + want: "Record>", + }, + { + name: "types", + input: "main.SomeType", + want: "main.SomeType", + }, + { + name: "primitive_generic", + input: "main.ListData[string]", + want: "main.ListData_string_", + }, + { + name: "stdlib_generic", + input: "main.ListData[*net/http.Request]", + want: "main.ListData_net_http_Request_", + }, + } + var importNamespaces slicer.StringSlicer + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := goTypeToJSDocType(tt.input, &importNamespaces); got != tt.want { + t.Errorf("goTypeToJSDocType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/internal/binding/parameter.go b/v2/internal/binding/parameter.go new file mode 100644 index 000000000..ef10a24ec --- /dev/null +++ b/v2/internal/binding/parameter.go @@ -0,0 +1,28 @@ +package binding + +import "reflect" + +// Parameter defines a Go method parameter +type Parameter struct { + Name string `json:"name,omitempty"` + TypeName string `json:"type"` + reflectType reflect.Type +} + +func newParameter(Name string, Type reflect.Type) *Parameter { + return &Parameter{ + Name: Name, + TypeName: Type.String(), + reflectType: Type, + } +} + +// IsType returns true if the given +func (p *Parameter) IsType(typename string) bool { + return p.TypeName == typename +} + +// IsError returns true if the parameter type is an error +func (p *Parameter) IsError() bool { + return p.IsType("error") +} diff --git a/v2/internal/binding/reflect.go b/v2/internal/binding/reflect.go new file mode 100644 index 000000000..c254d0f0a --- /dev/null +++ b/v2/internal/binding/reflect.go @@ -0,0 +1,200 @@ +package binding + +import ( + "fmt" + "reflect" + "runtime" + "strings" +) + +// isStructPtr returns true if the value given is a +// pointer to a struct +func isStructPtr(value interface{}) bool { + return reflect.ValueOf(value).Kind() == reflect.Ptr && + reflect.ValueOf(value).Elem().Kind() == reflect.Struct +} + +// isFunction returns true if the given value is a function +func isFunction(value interface{}) bool { + return reflect.ValueOf(value).Kind() == reflect.Func +} + +// isStruct returns true if the value given is a struct +func isStruct(value interface{}) bool { + return reflect.ValueOf(value).Kind() == reflect.Struct +} + +func normalizeStructName(name string) string { + return strings.ReplaceAll( + strings.ReplaceAll( + strings.ReplaceAll( + strings.ReplaceAll( + name, + ",", + "-", + ), + "*", + "", + ), + "]", + "__", + ), + "[", + "__", + ) +} + +func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { + // Create result placeholder + var result []*BoundMethod + + // Check type + if !isStructPtr(value) { + + if isStruct(value) { + name := reflect.ValueOf(value).Type().Name() + return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name) + } + + if isFunction(value) { + name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name() + return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name) + } + + return nil, fmt.Errorf("not a pointer to a struct.") + } + + // Process Struct + structType := reflect.TypeOf(value) + structValue := reflect.ValueOf(value) + structName := structType.Elem().Name() + structNameNormalized := normalizeStructName(structName) + pkgPath := strings.TrimSuffix(structType.Elem().String(), fmt.Sprintf(".%s", structName)) + + // Process Methods + for i := 0; i < structType.NumMethod(); i++ { + methodDef := structType.Method(i) + methodName := methodDef.Name + method := structValue.MethodByName(methodName) + + methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name() + if b.exemptions.Contains(methodReflectName) { + continue + } + + // Create new method + boundMethod := &BoundMethod{ + Path: &BoundedMethodPath{ + Package: pkgPath, + Struct: structNameNormalized, + Name: methodName, + }, + Inputs: nil, + Outputs: nil, + Comments: "", + Method: method, + } + + // Iterate inputs + methodType := method.Type() + inputParamCount := methodType.NumIn() + var inputs []*Parameter + for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ { + input := methodType.In(inputIndex) + thisParam := newParameter("", input) + + thisInput := input + + if thisInput.Kind() == reflect.Slice { + thisInput = thisInput.Elem() + } + + // Process struct pointer params + if thisInput.Kind() == reflect.Ptr { + if thisInput.Elem().Kind() == reflect.Struct { + typ := thisInput.Elem() + a := reflect.New(typ) + s := reflect.Indirect(a).Interface() + name := typ.Name() + packageName := getPackageName(thisInput.String()) + b.AddStructToGenerateTS(packageName, name, s) + } + } + + // Process struct params + if thisInput.Kind() == reflect.Struct { + a := reflect.New(thisInput) + s := reflect.Indirect(a).Interface() + name := thisInput.Name() + packageName := getPackageName(thisInput.String()) + b.AddStructToGenerateTS(packageName, name, s) + } + + inputs = append(inputs, thisParam) + } + + boundMethod.Inputs = inputs + + // Iterate outputs + // TODO: Determine what to do about limiting return types + // especially around errors. + outputParamCount := methodType.NumOut() + var outputs []*Parameter + for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ { + output := methodType.Out(outputIndex) + thisParam := newParameter("", output) + + thisOutput := output + + if thisOutput.Kind() == reflect.Slice { + thisOutput = thisOutput.Elem() + } + + // Process struct pointer params + if thisOutput.Kind() == reflect.Ptr { + if thisOutput.Elem().Kind() == reflect.Struct { + typ := thisOutput.Elem() + a := reflect.New(typ) + s := reflect.Indirect(a).Interface() + name := typ.Name() + packageName := getPackageName(thisOutput.String()) + b.AddStructToGenerateTS(packageName, name, s) + } + } + + // Process struct params + if thisOutput.Kind() == reflect.Struct { + a := reflect.New(thisOutput) + s := reflect.Indirect(a).Interface() + name := thisOutput.Name() + packageName := getPackageName(thisOutput.String()) + b.AddStructToGenerateTS(packageName, name, s) + } + + outputs = append(outputs, thisParam) + } + boundMethod.Outputs = outputs + + // Save method in result + result = append(result, boundMethod) + + } + return result, nil +} + +func getPackageName(in string) string { + result := strings.Split(in, ".")[0] + result = strings.ReplaceAll(result, "[]", "") + result = strings.ReplaceAll(result, "*", "") + return result +} + +func getSplitReturn(in string) (string, string) { + result := strings.SplitN(in, ".", 2) + return result[0], result[1] +} + +func hasElements(typ reflect.Type) bool { + kind := typ.Kind() + return kind == reflect.Ptr || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map +} diff --git a/v2/internal/colour/colour.go b/v2/internal/colour/colour.go new file mode 100644 index 000000000..ee1ca9743 --- /dev/null +++ b/v2/internal/colour/colour.go @@ -0,0 +1,145 @@ +package colour + +import ( + "fmt" + "strings" + + "github.com/wzshiming/ctc" +) + +var ColourEnabled = true + +func Col(col ctc.Color, text string) string { + if !ColourEnabled { + return text + } + return fmt.Sprintf("%s%s%s", col, text, ctc.Reset) +} + +func Yellow(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightYellow, text) +} + +func Red(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightRed, text) +} + +func Blue(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightBlue, text) +} + +func Green(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightGreen, text) +} + +func Cyan(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightCyan, text) +} + +func Magenta(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightMagenta, text) +} + +func White(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightWhite, text) +} + +func Black(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBrightBlack, text) +} + +func DarkYellow(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundYellow, text) +} + +func DarkRed(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundRed, text) +} + +func DarkBlue(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBlue, text) +} + +func DarkGreen(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundGreen, text) +} + +func DarkCyan(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundCyan, text) +} + +func DarkMagenta(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundMagenta, text) +} + +func DarkWhite(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundWhite, text) +} + +func DarkBlack(text string) string { + if !ColourEnabled { + return text + } + return Col(ctc.ForegroundBlack, text) +} + +var rainbowCols = []func(string) string{Red, Yellow, Green, Cyan, Blue, Magenta} + +func Rainbow(text string) string { + if !ColourEnabled { + return text + } + var builder strings.Builder + + for i := 0; i < len(text); i++ { + fn := rainbowCols[i%len(rainbowCols)] + builder.WriteString(fn(text[i : i+1])) + } + + return builder.String() +} diff --git a/v2/internal/frontend/calls.go b/v2/internal/frontend/calls.go new file mode 100644 index 000000000..5401106bc --- /dev/null +++ b/v2/internal/frontend/calls.go @@ -0,0 +1,5 @@ +package frontend + +type Calls interface { + Callback(message string) +} diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h new file mode 100644 index 000000000..a8d10f647 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -0,0 +1,33 @@ +// +// AppDelegate.h +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#ifndef AppDelegate_h +#define AppDelegate_h + +#import +#import "WailsContext.h" + +@interface AppDelegate : NSResponder + +@property bool alwaysOnTop; +@property bool startHidden; +@property (retain) NSString* singleInstanceUniqueId; +@property bool singleInstanceLockEnabled; +@property bool startFullscreen; +@property (retain) WailsWindow* mainWindow; + +@end + +extern void HandleOpenFile(char *); + +extern void HandleSecondInstanceData(char * message); + +void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); + +char* GetMacOsNativeTempDir(); + +#endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m new file mode 100644 index 000000000..a73ec3ec3 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -0,0 +1,100 @@ +// +// AppDelegate.m +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#import +#import + +#import "AppDelegate.h" +#import "CustomProtocol.h" +#import "message.h" + +@implementation AppDelegate +-(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename +{ + const char* utf8FileName = filename.UTF8String; + HandleOpenFile((char*)utf8FileName); + return YES; +} + +- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler { + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + NSURL *url = userActivity.webpageURL; + if (url) { + HandleOpenURL((char*)[[url absoluteString] UTF8String]); + return YES; + } + } + return NO; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { + return NO; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + processMessage("Q"); + return NSTerminateCancel; +} + +- (void)applicationWillFinishLaunching:(NSNotification *)aNotification { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + if (self.alwaysOnTop) { + [self.mainWindow setLevel:NSFloatingWindowLevel]; + } + if ( !self.startHidden ) { + [self.mainWindow makeKeyAndOrderFront:self]; + } +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [NSApp activateIgnoringOtherApps:YES]; + if ( self.startFullscreen ) { + NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior]; + behaviour |= NSWindowCollectionBehaviorFullScreenPrimary; + [self.mainWindow setCollectionBehavior:behaviour]; + [self.mainWindow toggleFullScreen:nil]; + } + + if ( self.singleInstanceLockEnabled ) { + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil]; + } +} + +void SendDataToFirstInstance(char * singleInstanceUniqueId, char * message) { + // we pass message in object because otherwise sandboxing will prevent us from sending it https://developer.apple.com/forums/thread/129437 + NSString * myString = [NSString stringWithUTF8String:message]; + [[NSDistributedNotificationCenter defaultCenter] + postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId] + object:(__bridge const void *)(myString) + userInfo:nil + deliverImmediately:YES]; +} + +char* GetMacOsNativeTempDir() { + NSString *tempDir = NSTemporaryDirectory(); + char *copy = strdup([tempDir UTF8String]); + + return copy; +} + +- (void)handleSecondInstanceNotification:(NSNotification *)note; +{ + if (note.object != nil) { + NSString * message = (__bridge NSString *)note.object; + const char* utf8Message = message.UTF8String; + HandleSecondInstanceData((char*)utf8Message); + } +} + +- (void)dealloc { + [super dealloc]; +} + +@synthesize touchBar; + +@end diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h new file mode 100644 index 000000000..4d8bbd37b --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -0,0 +1,74 @@ +// +// Application.h +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#ifndef Application_h +#define Application_h + +#import +#import +#import "WailsContext.h" + +#define WindowStartsNormal 0 +#define WindowStartsMaximised 1 +#define WindowStartsMinimised 2 +#define WindowStartsFullscreen 3 + +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop); +void Run(void*, const char* url); + +void SetTitle(void* ctx, const char *title); +void Center(void* ctx); +void SetSize(void* ctx, int width, int height); +void SetAlwaysOnTop(void* ctx, int onTop); +void SetMinSize(void* ctx, int width, int height); +void SetMaxSize(void* ctx, int width, int height); +void SetPosition(void* ctx, int x, int y); +void Fullscreen(void* ctx); +void UnFullscreen(void* ctx); +void Minimise(void* ctx); +void UnMinimise(void* ctx); +void ToggleMaximise(void* ctx); +void Maximise(void* ctx); +void UnMaximise(void* ctx); +void Hide(void* ctx); +void Show(void* ctx); +void HideApplication(void* ctx); +void ShowApplication(void* ctx); +void SetBackgroundColour(void* ctx, int r, int g, int b, int a); +void ExecJS(void* ctx, const char*); +void Quit(void*); +void WindowPrint(void* ctx); + +const char* GetSize(void *ctx); +const char* GetPosition(void *ctx); +const bool IsFullScreen(void *ctx); +const bool IsMinimised(void *ctx); +const bool IsMaximised(void *ctx); + +/* Dialogs */ + +void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength); +void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters); +void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters); + +/* Application Menu */ +void* NewMenu(const char* name); +void AppendSubmenu(void* parent, void* child); +void AppendRole(void *inctx, void *inMenu, int role); +void SetAsApplicationMenu(void *inctx, void *inMenu); +void UpdateApplicationMenu(void *inctx); + +void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen); +void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID); +void AppendSeparator(void* inMenu); +void UpdateMenuItem(void* nsmenuitem, int checked); +void RunMainLoop(void); +void ReleaseContext(void *inctx); + +NSString* safeInit(const char* input); + +#endif /* Application_h */ diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m new file mode 100644 index 000000000..38d349c2c --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -0,0 +1,433 @@ +//go:build darwin +// +// Application.m +// +// Created by Lea Anthony on 10/10/21. +// + +#import +#import +#import "WailsContext.h" +#import "Application.h" +#import "AppDelegate.h" +#import "WindowDelegate.h" +#import "WailsMenu.h" +#import "WailsMenuItem.h" + +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop) { + + [NSApplication sharedApplication]; + + WailsContext *result = [WailsContext new]; + + result.devtoolsEnabled = devtoolsEnabled; + result.defaultContextMenuEnabled = defaultContextMenuEnabled; + + if ( windowStartState == WindowStartsFullscreen ) { + fullscreen = 1; + } + + [result CreateWindow:width :height :frameless :resizable :zoomable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences :enableDragAndDrop :disableWebViewDragAndDrop]; + [result SetTitle:safeInit(title)]; + [result Center]; + + if (contentProtection == 1 && + [result.mainWindow respondsToSelector:@selector(setSharingType:)]) { + [result.mainWindow setSharingType:NSWindowSharingNone]; + } + + switch( windowStartState ) { + case WindowStartsMaximised: + [result.mainWindow zoom:nil]; + break; + case WindowStartsMinimised: + //TODO: Can you start a mac app minimised? + break; + } + + if ( startsHidden == 1 ) { + result.startHidden = true; + } + + if ( fullscreen == 1 ) { + result.startFullscreen = true; + } + + if ( singleInstanceLockEnabled == 1 ) { + result.singleInstanceLockEnabled = true; + result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId); + } + + result.alwaysOnTop = alwaysOnTop; + result.hideOnClose = hideWindowOnClose; + + return result; +} + +void ExecJS(void* inctx, const char *script) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSString *nsscript = safeInit(script); + ON_MAIN_THREAD( + [ctx ExecJS:nsscript]; + [nsscript release]; + ); +} + +void SetTitle(void* inctx, const char *title) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSString *_title = safeInit(title); + ON_MAIN_THREAD( + [ctx SetTitle:_title]; + ); +} + + +void SetBackgroundColour(void *inctx, int r, int g, int b, int a) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetBackgroundColour:r :g :b :a]; + ); +} + +void SetSize(void* inctx, int width, int height) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetSize:width :height]; + ); +} + +void SetAlwaysOnTop(void* inctx, int onTop) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetAlwaysOnTop:onTop]; + ); +} + +void SetMinSize(void* inctx, int width, int height) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetMinSize:width :height]; + ); +} + +void SetMaxSize(void* inctx, int width, int height) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetMaxSize:width :height]; + ); +} + +void SetPosition(void* inctx, int x, int y) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx SetPosition:x :y]; + ); +} + +void Center(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Center]; + ); +} + +void Fullscreen(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Fullscreen]; + ); +} + +void UnFullscreen(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx UnFullscreen]; + ); +} + +void Minimise(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Minimise]; + ); +} + +void UnMinimise(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx UnMinimise]; + ); +} + +void Maximise(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Maximise]; + ); +} + +void ToggleMaximise(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx ToggleMaximise]; + ); +} + +const char* GetSize(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSRect frame = [ctx.mainWindow frame]; + NSString *result = [NSString stringWithFormat:@"%d,%d", (int)frame.size.width, (int)frame.size.height]; + return [result UTF8String]; +} + +const char* GetPosition(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSScreen* screen = [ctx getCurrentScreen]; + NSRect windowFrame = [ctx.mainWindow frame]; + NSRect screenFrame = [screen visibleFrame]; + int x = windowFrame.origin.x - screenFrame.origin.x; + int y = windowFrame.origin.y - screenFrame.origin.y; + y = screenFrame.size.height - y - windowFrame.size.height; + NSString *result = [NSString stringWithFormat:@"%d,%d",x,y]; + return [result UTF8String]; +} + +const bool IsFullScreen(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + return [ctx IsFullScreen]; +} + +const bool IsMinimised(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + return [ctx IsMinimised]; +} + +const bool IsMaximised(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + return [ctx IsMaximised]; +} + +void UnMaximise(void* inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx UnMaximise]; + ); +} + +void Quit(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + [NSApp stop:ctx]; + [NSApp abortModal]; +} + +void Hide(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Hide]; + ); +} + +void Show(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx Show]; + ); +} + + +void HideApplication(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx HideApplication]; + ); +} + +void ShowApplication(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + [ctx ShowApplication]; + ); +} + +NSString* safeInit(const char* input) { + NSString *result = nil; + if (input != nil) { + result = [NSString stringWithUTF8String:input]; + } + return result; +} + +void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + + NSString *_dialogType = safeInit(dialogType); + NSString *_title = safeInit(title); + NSString *_message = safeInit(message); + NSString *_button1 = safeInit(button1); + NSString *_button2 = safeInit(button2); + NSString *_button3 = safeInit(button3); + NSString *_button4 = safeInit(button4); + NSString *_defaultButton = safeInit(defaultButton); + NSString *_cancelButton = safeInit(cancelButton); + + ON_MAIN_THREAD( + [ctx MessageDialog:_dialogType :_title :_message :_button1 :_button2 :_button3 :_button4 :_defaultButton :_cancelButton :iconData :iconDataLength]; + ) +} + +void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters) { + + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSString *_title = safeInit(title); + NSString *_defaultFilename = safeInit(defaultFilename); + NSString *_defaultDirectory = safeInit(defaultDirectory); + NSString *_filters = safeInit(filters); + + ON_MAIN_THREAD( + [ctx OpenFileDialog:_title :_defaultFilename :_defaultDirectory :allowDirectories :allowFiles :canCreateDirectories :treatPackagesAsDirectories :resolveAliases :showHiddenFiles :allowMultipleSelection :_filters]; + ) +} + +void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters) { + + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSString *_title = safeInit(title); + NSString *_defaultFilename = safeInit(defaultFilename); + NSString *_defaultDirectory = safeInit(defaultDirectory); + NSString *_filters = safeInit(filters); + + ON_MAIN_THREAD( + [ctx SaveFileDialog:_title :_defaultFilename :_defaultDirectory :canCreateDirectories :treatPackagesAsDirectories :showHiddenFiles :_filters]; + ) +} + +void AppendRole(void *inctx, void *inMenu, int role) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + WailsMenu *menu = (__bridge WailsMenu*) inMenu; + [menu appendRole :ctx :role]; +} + +void* NewMenu(const char *name) { + NSString *title = @""; + if (name != nil) { + title = [NSString stringWithUTF8String:name]; + } + WailsMenu *result = [[WailsMenu new] initWithNSTitle:title]; + return result; +} + +void AppendSubmenu(void* inparent, void* inchild) { + WailsMenu *parent = (__bridge WailsMenu*) inparent; + WailsMenu *child = (__bridge WailsMenu*) inchild; + [parent appendSubmenu:child]; +} + +void SetAsApplicationMenu(void *inctx, void *inMenu) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + WailsMenu *menu = (__bridge WailsMenu*) inMenu; + ctx.applicationMenu = menu; +} + +void UpdateApplicationMenu(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + ON_MAIN_THREAD( + NSApplication *app = [NSApplication sharedApplication]; + [app setMainMenu:ctx.applicationMenu]; + ) +} + +void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSString *_title = safeInit(title); + NSString *_description = safeInit(description); + + [ctx SetAbout :_title :_description :imagedata :datalen]; +} + +void* AppendMenuItem(void* inctx, void* inMenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + WailsMenu *menu = (__bridge WailsMenu*) inMenu; + NSString *_label = safeInit(label); + NSString *_shortcutKey = safeInit(shortcutKey); + + return [menu AppendMenuItem:ctx :_label :_shortcutKey :modifiers :disabled :checked :menuItemID]; +} + +void UpdateMenuItem(void* nsmenuitem, int checked) { + ON_MAIN_THREAD( + WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem; + [menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)]; + ) +} + + +void AppendSeparator(void* inMenu) { + WailsMenu *menu = (__bridge WailsMenu*) inMenu; + [menu AppendSeparator]; +} + + + +void Run(void *inctx, const char* url) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSApplication *app = [NSApplication sharedApplication]; + AppDelegate* delegate = [AppDelegate new]; + [app setDelegate:(id)delegate]; + ctx.appdelegate = delegate; + delegate.mainWindow = ctx.mainWindow; + delegate.alwaysOnTop = ctx.alwaysOnTop; + delegate.startHidden = ctx.startHidden; + delegate.singleInstanceLockEnabled = ctx.singleInstanceLockEnabled; + delegate.singleInstanceUniqueId = ctx.singleInstanceUniqueId; + delegate.startFullscreen = ctx.startFullscreen; + + NSString *_url = safeInit(url); + [ctx loadRequest:_url]; + [_url release]; + + [app setMainMenu:ctx.applicationMenu]; +} + +void RunMainLoop(void) { + NSApplication *app = [NSApplication sharedApplication]; + [app run]; +} + +void ReleaseContext(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + [ctx release]; +} + +// Credit: https://stackoverflow.com/q/33319295 +void WindowPrint(void *inctx) { + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 + if (@available(macOS 11.0, *)) { + ON_MAIN_THREAD( + WailsContext *ctx = (__bridge WailsContext*) inctx; + WKWebView* webView = ctx.webview; + + // I think this should be exposed as a config + // It directly affects the printed output/PDF + NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo]; + pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic; + pInfo.verticalPagination = NSPrintingPaginationModeAutomatic; + pInfo.verticallyCentered = YES; + pInfo.horizontallyCentered = YES; + pInfo.orientation = NSPaperOrientationLandscape; + pInfo.leftMargin = 0; + pInfo.rightMargin = 0; + pInfo.topMargin = 0; + pInfo.bottomMargin = 0; + + NSPrintOperation *po = [webView printOperationWithPrintInfo:pInfo]; + po.showsPrintPanel = YES; + po.showsProgressPanel = YES; + + po.view.frame = webView.bounds; + + [po runOperationModalForWindow:ctx.mainWindow delegate:ctx.mainWindow.delegate didRunSelector:nil contextInfo:nil]; + ) + } +#endif +} diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.h b/v2/internal/frontend/desktop/darwin/CustomProtocol.h new file mode 100644 index 000000000..0698a4d45 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.h @@ -0,0 +1,14 @@ +#ifndef CustomProtocol_h +#define CustomProtocol_h + +#import + +extern void HandleOpenURL(char*); + +@interface CustomProtocolSchemeHandler : NSObject ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +@end + +void StartCustomProtocolHandler(void); + +#endif /* CustomProtocol_h */ diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m new file mode 100644 index 000000000..ebc61aa00 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -0,0 +1,20 @@ +#include "CustomProtocol.h" + +@implementation CustomProtocolSchemeHandler ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [event paramDescriptorForKeyword:keyDirectObject]; + + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + HandleOpenURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); +} +@end + +void StartCustomProtocolHandler(void) { + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + + [appleEventManager setEventHandler:[CustomProtocolSchemeHandler class] + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID: kAEGetURL]; +} diff --git a/v2/internal/frontend/desktop/darwin/Role.h b/v2/internal/frontend/desktop/darwin/Role.h new file mode 100644 index 000000000..6b8877a09 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/Role.h @@ -0,0 +1,17 @@ +// +// Role.h +// test +// +// Created by Lea Anthony on 24/10/21. +// + +#ifndef Role_h +#define Role_h + +typedef int Role; + +static const Role AppMenu = 1; +static const Role EditMenu = 2; +static const Role WindowMenu = 3; + +#endif /* Role_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsAlert.h b/v2/internal/frontend/desktop/darwin/WailsAlert.h new file mode 100644 index 000000000..29dc839f6 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsAlert.h @@ -0,0 +1,18 @@ +// +// WailsAlert.h +// test +// +// Created by Lea Anthony on 20/10/21. +// + +#ifndef WailsAlert_h +#define WailsAlert_h + +#import + +@interface WailsAlert : NSAlert +- (void)addButton:(NSString*)text :(NSString*)defaultButton :(NSString*)cancelButton; +@end + + +#endif /* WailsAlert_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsAlert.m b/v2/internal/frontend/desktop/darwin/WailsAlert.m new file mode 100644 index 000000000..3c8b7305a --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsAlert.m @@ -0,0 +1,31 @@ +//go:build darwin +// +// WailsAlert.m +// test +// +// Created by Lea Anthony on 20/10/21. +// + +#import + +#import "WailsAlert.h" + +@implementation WailsAlert + +- (void)addButton:(NSString*)text :(NSString*)defaultButton :(NSString*)cancelButton { + if( text == nil ) { + return; + } + NSButton *button = [self addButtonWithTitle:text]; + if( defaultButton != nil && [text isEqualToString:defaultButton]) { + [button setKeyEquivalent:@"\r"]; + } else if( cancelButton != nil && [text isEqualToString:cancelButton]) { + [button setKeyEquivalent:@"\033"]; + } else { + [button setKeyEquivalent:@""]; + } +} + +@end + + diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.h b/v2/internal/frontend/desktop/darwin/WailsContext.h new file mode 100644 index 000000000..2ec6d8707 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -0,0 +1,109 @@ +// +// WailsContext.h +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#ifndef WailsContext_h +#define WailsContext_h + +#import +#import +#import "WailsWebView.h" + +#if __has_include() +#define USE_NEW_FILTERS +#import +#endif + +#define ON_MAIN_THREAD(str) dispatch_async(dispatch_get_main_queue(), ^{ str; }); +#define unicode(input) [NSString stringWithFormat:@"%C", input] + +@interface WailsWindow : NSWindow + +@property NSSize userMinSize; +@property NSSize userMaxSize; + +- (BOOL) canBecomeKeyWindow; +- (void) applyWindowConstraints; +- (void) disableWindowConstraints; +@end + +@interface WailsContext : NSObject + +@property (retain) WailsWindow* mainWindow; +@property (retain) WailsWebView* webview; +@property (nonatomic, assign) id appdelegate; + +@property bool hideOnClose; +@property bool shuttingDown; +@property bool startHidden; +@property bool startFullscreen; + +@property bool singleInstanceLockEnabled; +@property (retain) NSString* singleInstanceUniqueId; + +@property (retain) NSEvent* mouseEvent; + +@property bool alwaysOnTop; + +@property bool devtoolsEnabled; +@property bool defaultContextMenuEnabled; + +@property (retain) WKUserContentController* userContentController; + +@property (retain) NSMenu* applicationMenu; + +@property (retain) NSImage* aboutImage; +@property (retain) NSString* aboutTitle; +@property (retain) NSString* aboutDescription; + +struct Preferences { + bool *tabFocusesLinks; + bool *textInteractionEnabled; + bool *fullscreenEnabled; +}; + +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences :(bool)enableDragAndDrop :(bool)disableWebViewDragAndDrop; +- (void) SetSize:(int)width :(int)height; +- (void) SetPosition:(int)x :(int) y; +- (void) SetMinSize:(int)minWidth :(int)minHeight; +- (void) SetMaxSize:(int)maxWidth :(int)maxHeight; +- (void) SetTitle:(NSString*)title; +- (void) SetAlwaysOnTop:(int)onTop; +- (void) Center; +- (void) Fullscreen; +- (void) UnFullscreen; +- (bool) IsFullScreen; +- (void) Minimise; +- (void) UnMinimise; +- (bool) IsMinimised; +- (void) Maximise; +- (void) ToggleMaximise; +- (void) UnMaximise; +- (bool) IsMaximised; +- (void) SetBackgroundColour:(int)r :(int)g :(int)b :(int)a; +- (void) HideMouse; +- (void) ShowMouse; +- (void) Hide; +- (void) Show; +- (void) HideApplication; +- (void) ShowApplication; +- (void) Quit; + +-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength; +- (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters; +- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; + +- (void) loadRequest:(NSString*)url; +- (void) ExecJS:(NSString*)script; +- (NSScreen*) getCurrentScreen; + +- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen; +- (void) dealloc; + +@end + + +#endif /* WailsContext_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.m b/v2/internal/frontend/desktop/darwin/WailsContext.m new file mode 100644 index 000000000..7c9660d54 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsContext.m @@ -0,0 +1,755 @@ +// +// WailsContext.m +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#import +#import +#import "WailsContext.h" +#import "WailsAlert.h" +#import "WailsMenu.h" +#import "WailsWebView.h" +#import "WindowDelegate.h" +#import "message.h" +#import "Role.h" + +typedef void (^schemeTaskCaller)(id); + +@implementation WailsWindow + +- (BOOL)canBecomeKeyWindow +{ + return YES; +} + +- (void) applyWindowConstraints { + [self setMinSize:self.userMinSize]; + [self setMaxSize:self.userMaxSize]; +} + +- (void) disableWindowConstraints { + [self setMinSize:NSMakeSize(0, 0)]; + [self setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; +} + +@end + +@implementation WailsContext + +- (void) SetSize:(int)width :(int)height { + + if (self.shuttingDown) return; + + NSRect frame = [self.mainWindow frame]; + frame.origin.y += frame.size.height - height; + frame.size.width = width; + frame.size.height = height; + [self.mainWindow setFrame:frame display:TRUE animate:FALSE]; +} + +- (void) SetPosition:(int)x :(int)y { + + if (self.shuttingDown) return; + + NSScreen* screen = [self getCurrentScreen]; + NSRect windowFrame = [self.mainWindow frame]; + NSRect screenFrame = [screen visibleFrame]; + windowFrame.origin.x = screenFrame.origin.x + (float)x; + windowFrame.origin.y = (screenFrame.origin.y + screenFrame.size.height) - windowFrame.size.height - (float)y; + + [self.mainWindow setFrame:windowFrame display:TRUE animate:FALSE]; +} + +- (void) SetMinSize:(int)minWidth :(int)minHeight { + + if (self.shuttingDown) return; + + NSSize size = { minWidth, minHeight }; + self.mainWindow.userMinSize = size; + [self.mainWindow setMinSize:size]; + [self adjustWindowSize]; +} + + +- (void) SetMaxSize:(int)maxWidth :(int)maxHeight { + + if (self.shuttingDown) return; + + NSSize size = { FLT_MAX, FLT_MAX }; + + size.width = maxWidth > 0 ? maxWidth : FLT_MAX; + size.height = maxHeight > 0 ? maxHeight : FLT_MAX; + + self.mainWindow.userMaxSize = size; + [self.mainWindow setMaxSize:size]; + [self adjustWindowSize]; +} + + +- (void) adjustWindowSize { + + if (self.shuttingDown) return; + + NSRect currentFrame = [self.mainWindow frame]; + + if ( currentFrame.size.width > self.mainWindow.userMaxSize.width ) currentFrame.size.width = self.mainWindow.userMaxSize.width; + if ( currentFrame.size.width < self.mainWindow.userMinSize.width ) currentFrame.size.width = self.mainWindow.userMinSize.width; + if ( currentFrame.size.height > self.mainWindow.userMaxSize.height ) currentFrame.size.height = self.mainWindow.userMaxSize.height; + if ( currentFrame.size.height < self.mainWindow.userMinSize.height ) currentFrame.size.height = self.mainWindow.userMinSize.height; + + [self.mainWindow setFrame:currentFrame display:YES animate:FALSE]; + +} + +- (void) dealloc { + [self.appdelegate release]; + [self.mainWindow release]; + [self.mouseEvent release]; + [self.userContentController release]; + [self.applicationMenu release]; + [super dealloc]; +} + +- (NSScreen*) getCurrentScreen { + NSScreen* screen = [self.mainWindow screen]; + if( screen == NULL ) { + screen = [NSScreen mainScreen]; + } + return screen; +} + +- (void) SetTitle:(NSString*)title { + [self.mainWindow setTitle:title]; +} + +- (void) Center { + [self.mainWindow center]; +} + +- (BOOL) isFullscreen { + NSWindowStyleMask masks = [self.mainWindow styleMask]; + if ( masks & NSWindowStyleMaskFullScreen ) { + return YES; + } + return NO; +} + +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences :(bool)enableDragAndDrop :(bool)disableWebViewDragAndDrop { + NSWindowStyleMask styleMask = 0; + + if( !frameless ) { + if (!hideTitleBar) { + styleMask |= NSWindowStyleMaskTitled; + } + styleMask |= NSWindowStyleMaskClosable; + } + + styleMask |= NSWindowStyleMaskMiniaturizable; + + if( fullSizeContent || frameless || titlebarAppearsTransparent ) { + styleMask |= NSWindowStyleMaskFullSizeContentView; + } + + if (resizable) { + styleMask |= NSWindowStyleMaskResizable; + } + + self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height) + styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; + if (!frameless && useToolbar) { + id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"]; + [toolbar autorelease]; + [toolbar setShowsBaselineSeparator:!hideToolbarSeparator]; + [self.mainWindow setToolbar:toolbar]; + + } + + [self.mainWindow setTitleVisibility:hideTitle]; + [self.mainWindow setTitlebarAppearsTransparent:titlebarAppearsTransparent]; + +// [self.mainWindow canBecomeKeyWindow]; + + id contentView = [self.mainWindow contentView]; + if (windowIsTranslucent) { + NSVisualEffectView *effectView = [NSVisualEffectView alloc]; + NSRect bounds = [contentView bounds]; + [effectView initWithFrame:bounds]; + [effectView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [effectView setState:NSVisualEffectStateActive]; + [contentView addSubview:effectView positioned:NSWindowBelow relativeTo:nil]; + } + + if (appearance != nil) { + NSAppearance *nsAppearance = [NSAppearance appearanceNamed:appearance]; + [self.mainWindow setAppearance:nsAppearance]; + } + + if (!zoomable && resizable) { + NSButton *button = [self.mainWindow standardWindowButton:NSWindowZoomButton]; + [button setEnabled: NO]; + } + + + NSSize minSize = { minWidth, minHeight }; + NSSize maxSize = { maxWidth, maxHeight }; + if (maxSize.width == 0) { + maxSize.width = FLT_MAX; + } + if (maxSize.height == 0) { + maxSize.height = FLT_MAX; + } + self.mainWindow.userMaxSize = maxSize; + self.mainWindow.userMinSize = minSize; + + if( !fullscreen ) { + [self.mainWindow applyWindowConstraints]; + } + + WindowDelegate *windowDelegate = [WindowDelegate new]; + windowDelegate.hideOnClose = hideWindowOnClose; + windowDelegate.ctx = self; + [self.mainWindow setDelegate:windowDelegate]; + + // Webview stuff here! + WKWebViewConfiguration *config = [WKWebViewConfiguration new]; + // Disable suppressesIncrementalRendering on macOS 26+ to prevent WebView crashes + // during rapid UI updates. See: https://github.com/wailsapp/wails/issues/4592 + if (@available(macOS 26.0, *)) { + config.suppressesIncrementalRendering = false; + } else { + config.suppressesIncrementalRendering = true; + } + config.applicationNameForUserAgent = @"wails.io"; + [config setURLSchemeHandler:self forURLScheme:@"wails"]; + + if (preferences.tabFocusesLinks != NULL) { + config.preferences.tabFocusesLinks = *preferences.tabFocusesLinks; + } + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110300 + if (@available(macOS 11.3, *)) { + if (preferences.textInteractionEnabled != NULL) { + config.preferences.textInteractionEnabled = *preferences.textInteractionEnabled; + } + } +#endif + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120300 + if (@available(macOS 12.3, *)) { + if (preferences.fullscreenEnabled != NULL) { + config.preferences.elementFullscreenEnabled = *preferences.fullscreenEnabled; + } + } +#endif + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 + if (@available(macOS 10.15, *)) { + config.preferences.fraudulentWebsiteWarningEnabled = fraudulentWebsiteWarningEnabled; + } +#endif + + WKUserContentController* userContentController = [WKUserContentController new]; + [userContentController addScriptMessageHandler:self name:@"external"]; + config.userContentController = userContentController; + self.userContentController = userContentController; + + if (self.devtoolsEnabled) { + [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; + } + + if (!self.defaultContextMenuEnabled) { + // Disable default context menus + WKUserScript *initScript = [WKUserScript new]; + [initScript initWithSource:@"window.wails.flags.disableDefaultContextMenu = true;" + injectionTime:WKUserScriptInjectionTimeAtDocumentEnd + forMainFrameOnly:false]; + [userContentController addUserScript:initScript]; + } + + self.webview = [WailsWebView alloc]; + self.webview.enableDragAndDrop = enableDragAndDrop; + self.webview.disableWebViewDragAndDrop = disableWebViewDragAndDrop; + + CGRect init = { 0,0,0,0 }; + [self.webview initWithFrame:init configuration:config]; + [contentView addSubview:self.webview]; + [self.webview setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable]; + CGRect contentViewBounds = [contentView bounds]; + [self.webview setFrame:contentViewBounds]; + + if (webviewIsTransparent) { + [self.webview setValue:[NSNumber numberWithBool:!webviewIsTransparent] forKey:@"drawsBackground"]; + } + + [self.webview setNavigationDelegate:self]; + self.webview.UIDelegate = self; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:FALSE forKey:@"NSAutomaticQuoteSubstitutionEnabled"]; + + // Mouse monitors + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + id window = [event window]; + if (window == self.mainWindow) { + self.mouseEvent = event; + } + return event; + }]; + + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + id window = [event window]; + if (window == self.mainWindow) { + self.mouseEvent = nil; + [self ShowMouse]; + } + return event; + }]; + + self.applicationMenu = [NSMenu new]; + +} + +- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags { + NSMenuItem *result = [[[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key] autorelease]; + if( flags != 0 ) { + [result setKeyEquivalentModifierMask:flags]; + } + return result; +} + +- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key { + return [self newMenuItem :title :selector :key :0]; +} + +- (NSMenu*) newMenu :(NSString*)title { + WailsMenu *result = [[WailsMenu new] initWithTitle:title]; + [result setAutoenablesItems:NO]; + return result; +} + +- (void) Quit { + processMessage("Q"); +} + +- (void) loadRequest :(NSString*)url { + NSURL *wkUrl = [NSURL URLWithString:url]; + NSURLRequest *wkRequest = [NSURLRequest requestWithURL:wkUrl]; + [self.webview loadRequest:wkRequest]; +} + +- (void) SetBackgroundColour:(int)r :(int)g :(int)b :(int)a { + float red = r/255.0; + float green = g/255.0; + float blue = b/255.0; + float alpha = a/255.0; + + id colour = [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha ]; + + [self.mainWindow setBackgroundColor:colour]; +} + +- (void) HideMouse { + [NSCursor hide]; +} + +- (void) ShowMouse { + [NSCursor unhide]; +} + +- (bool) IsFullScreen { + long mask = [self.mainWindow styleMask]; + return (mask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen; +} + +// Fullscreen sets the main window to be fullscreen +- (void) Fullscreen { + if( ! [self IsFullScreen] ) { + [self.mainWindow disableWindowConstraints]; + [self.mainWindow toggleFullScreen:nil]; + } +} + +// UnFullscreen resets the main window after a fullscreen +- (void) UnFullscreen { + if( [self IsFullScreen] ) { + [self.mainWindow applyWindowConstraints]; + [self.mainWindow toggleFullScreen:nil]; + } +} + +- (void) Minimise { + [self.mainWindow miniaturize:nil]; +} + +- (void) UnMinimise { + [self.mainWindow deminiaturize:nil]; +} + +- (bool) IsMinimised { + return [self.mainWindow isMiniaturized]; +} + +- (void) Hide { + [self.mainWindow orderOut:nil]; +} + +- (void) Show { + [self.mainWindow makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; +} + +- (void) HideApplication { + [[NSApplication sharedApplication] hide:self]; +} + +- (void) ShowApplication { + [[NSApplication sharedApplication] unhide:self]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE]; + +} + +- (void) Maximise { + if (![self.mainWindow isZoomed]) { + [self.mainWindow zoom:nil]; + } +} + +- (void) ToggleMaximise { + [self.mainWindow zoom:nil]; +} + +- (void) UnMaximise { + if ([self.mainWindow isZoomed]) { + [self.mainWindow zoom:nil]; + } +} + +- (void) SetAlwaysOnTop:(int)onTop { + if (onTop) { + [self.mainWindow setLevel:NSFloatingWindowLevel]; + } else { + [self.mainWindow setLevel:NSNormalWindowLevel]; + } +} + +- (bool) IsMaximised { + return [self.mainWindow isZoomed]; +} + +- (void) ExecJS:(NSString*)script { + [self.webview evaluateJavaScript:script completionHandler:nil]; +} + +- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters + initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray * URLs))completionHandler { + + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection; +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 + if (@available(macOS 10.14, *)) { + openPanel.canChooseDirectories = parameters.allowsDirectories; + } +#endif + [openPanel + beginSheetModalForWindow:webView.window + completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) + completionHandler(openPanel.URLs); + else + completionHandler(nil); + }]; +} + +- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id)urlSchemeTask { + // This callback is run with an autorelease pool + processURLRequest(self, urlSchemeTask); +} + +- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask { + NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; + if (stream) { + NSStreamStatus status = stream.streamStatus; + if (status != NSStreamStatusClosed && status != NSStreamStatusNotOpen) { + [stream close]; + } + } +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + processMessage("DomReady"); +} + +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + // Get the origin from the message's frame + NSString *origin = nil; + if (message.frameInfo && message.frameInfo.request && message.frameInfo.request.URL) { + NSURL *url = message.frameInfo.request.URL; + if (url.scheme && url.host) { + origin = [url absoluteString]; + } + } + + NSString *m = message.body; + + // Check for drag + if ( [m isEqualToString:@"drag"] ) { + if( [self IsFullScreen] ) { + return; + } + if( self.mouseEvent != nil ) { + [self.mainWindow performWindowDragWithEvent:self.mouseEvent]; + } + return; + } + + const char *_m = [m UTF8String]; + const char *_origin = [origin UTF8String]; + + processBindingMessage(_m, _origin, message.frameInfo.isMainFrame); +} + +/***** Dialogs ******/ +-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength { + + WailsAlert *alert = [WailsAlert new]; + + int style = NSAlertStyleInformational; + if (dialogType != nil ) { + if( [dialogType isEqualToString:@"warning"] ) { + style = NSAlertStyleWarning; + } + if( [dialogType isEqualToString:@"error"] ) { + style = NSAlertStyleCritical; + } + } + [alert setAlertStyle:style]; + if( title != nil ) { + [alert setMessageText:title]; + } + if( message != nil ) { + [alert setInformativeText:message]; + } + + [alert addButton:button1 :defaultButton :cancelButton]; + [alert addButton:button2 :defaultButton :cancelButton]; + [alert addButton:button3 :defaultButton :cancelButton]; + [alert addButton:button4 :defaultButton :cancelButton]; + + NSImage *icon = nil; + if (iconData != nil) { + NSData *imageData = [NSData dataWithBytes:iconData length:iconDataLength]; + icon = [[NSImage alloc] initWithData:imageData]; + } + if( icon != nil) { + [alert setIcon:icon]; + } + [alert.window setLevel:NSFloatingWindowLevel]; + + long response = [alert runModal]; + int result; + + if( response == NSAlertFirstButtonReturn ) { + result = 0; + } + else if( response == NSAlertSecondButtonReturn ) { + result = 1; + } + else if( response == NSAlertThirdButtonReturn ) { + result = 2; + } else { + result = 3; + } + processMessageDialogResponse(result); +} + +-(void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters { + + + // Create the dialog + NSOpenPanel *dialog = [NSOpenPanel openPanel]; + + // Valid but appears to do nothing.... :/ + if( title != nil ) { + [dialog setTitle:title]; + } + + // Filters - semicolon delimited list of file extensions + if( allowFiles ) { + if( filters != nil && [filters length] > 0) { + filters = [filters stringByReplacingOccurrencesOfString:@"*." withString:@""]; + filters = [filters stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSArray *filterList = [filters componentsSeparatedByString:@";"]; +#ifdef USE_NEW_FILTERS + NSMutableArray *contentTypes = [[NSMutableArray new] autorelease]; + for (NSString *filter in filterList) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 + if (@available(macOS 11.0, *)) { + UTType *t = [UTType typeWithFilenameExtension:filter]; + [contentTypes addObject:t]; + } +#endif + } +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 + if (@available(macOS 11.0, *)) { + [dialog setAllowedContentTypes:contentTypes]; + } +#endif +#else + [dialog setAllowedFileTypes:filterList]; +#endif + } else { + [dialog setAllowsOtherFileTypes:true]; + } + // Default Filename + if( defaultFilename != nil ) { + [dialog setNameFieldStringValue:defaultFilename]; + } + + [dialog setAllowsMultipleSelection: allowMultipleSelection]; + } + [dialog setShowsHiddenFiles: showHiddenFiles]; + + // Default Directory + if( defaultDirectory != nil ) { + NSURL *url = [NSURL fileURLWithPath:defaultDirectory]; + [dialog setDirectoryURL:url]; + } + + + // Setup Options + [dialog setCanChooseFiles: allowFiles]; + [dialog setCanChooseDirectories: allowDirectories]; + [dialog setCanCreateDirectories: canCreateDirectories]; + [dialog setResolvesAliases: resolveAliases]; + [dialog setTreatsFilePackagesAsDirectories: treatPackagesAsDirectories]; + + // Setup callback handler + [dialog beginSheetModalForWindow:self.mainWindow completionHandler:^(NSModalResponse returnCode) { + if ( returnCode != NSModalResponseOK) { + processOpenFileDialogResponse("[]"); + return; + } + NSMutableArray *arr = [NSMutableArray new]; + for (NSURL *url in [dialog URLs]) { + [arr addObject:[url path]]; + } + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:arr options:0 error:nil]; + NSString *nsjson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + processOpenFileDialogResponse([nsjson UTF8String]); + [nsjson release]; + [arr release]; + }]; + +} + + +-(void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; { + + + // Create the dialog + NSSavePanel *dialog = [NSSavePanel savePanel]; + + // Do not hide extension + [dialog setExtensionHidden:false]; + + // Valid but appears to do nothing.... :/ + if( title != nil ) { + [dialog setTitle:title]; + } + + // Filters - semicolon delimited list of file extensions + if( filters != nil && [filters length] > 0) { + filters = [filters stringByReplacingOccurrencesOfString:@"*." withString:@""]; + filters = [filters stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSArray *filterList = [filters componentsSeparatedByString:@";"]; +#ifdef USE_NEW_FILTERS + NSMutableArray *contentTypes = [[NSMutableArray new] autorelease]; + for (NSString *filter in filterList) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 + if (@available(macOS 11.0, *)) { + UTType *t = [UTType typeWithFilenameExtension:filter]; + [contentTypes addObject:t]; + } +#endif + } + if( contentTypes.count == 0) { + [dialog setAllowsOtherFileTypes:true]; + } else { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 + if (@available(macOS 11.0, *)) { + [dialog setAllowedContentTypes:contentTypes]; + } +#endif + } + +#else + [dialog setAllowedFileTypes:filterList]; +#endif + } else { + [dialog setAllowsOtherFileTypes:true]; + } + // Default Filename + if( defaultFilename != nil ) { + [dialog setNameFieldStringValue:defaultFilename]; + } + + // Default Directory + if( defaultDirectory != nil ) { + NSURL *url = [NSURL fileURLWithPath:defaultDirectory]; + [dialog setDirectoryURL:url]; + } + + // Setup Options + [dialog setCanSelectHiddenExtension:true]; +// dialog.isExtensionHidden = false; + [dialog setCanCreateDirectories: canCreateDirectories]; + [dialog setTreatsFilePackagesAsDirectories: treatPackagesAsDirectories]; + [dialog setShowsHiddenFiles: showHiddenFiles]; + + // Setup callback handler + [dialog beginSheetModalForWindow:self.mainWindow completionHandler:^(NSModalResponse returnCode) { + if ( returnCode == NSModalResponseOK ) { + NSURL *url = [dialog URL]; + if ( url != nil ) { + processSaveFileDialogResponse([url.path UTF8String]); + return; + } + } + processSaveFileDialogResponse(""); + }]; + +} + +- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen { + self.aboutTitle = title; + self.aboutDescription = description; + + NSData *imageData = [NSData dataWithBytes:imagedata length:datalen]; + self.aboutImage = [[NSImage alloc] initWithData:imageData]; +} + +-(void) About { + + WailsAlert *alert = [WailsAlert new]; + [alert setAlertStyle:NSAlertStyleInformational]; + if( self.aboutTitle != nil ) { + [alert setMessageText:self.aboutTitle]; + } + if( self.aboutDescription != nil ) { + [alert setInformativeText:self.aboutDescription]; + } + + + [alert.window setLevel:NSFloatingWindowLevel]; + if ( self.aboutImage != nil) { + [alert setIcon:self.aboutImage]; + } + + [alert runModal]; +} + +@end + diff --git a/v2/internal/frontend/desktop/darwin/WailsMenu.h b/v2/internal/frontend/desktop/darwin/WailsMenu.h new file mode 100644 index 000000000..8ef120356 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsMenu.h @@ -0,0 +1,30 @@ +// +// WailsMenu.h +// test +// +// Created by Lea Anthony on 25/10/21. +// + +#ifndef WailsMenu_h +#define WailsMenu_h + +#import +#import "Role.h" +#import "WailsMenu.h" +#import "WailsContext.h" + +@interface WailsMenu : NSMenu + +//- (void) AddMenuByRole :(Role)role; +- (WailsMenu*) initWithNSTitle :(NSString*)title; +- (void) appendSubmenu :(WailsMenu*)child; +- (void) appendRole :(WailsContext*)ctx :(Role)role; + +- (NSMenuItem*) newMenuItemWithContext :(WailsContext*)ctx :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags; +- (void*) AppendMenuItem :(WailsContext*)ctx :(NSString*)label :(NSString *)shortcutKey :(int)modifiers :(bool)disabled :(bool)checked :(int)menuItemID; +- (void) AppendSeparator; + +@end + + +#endif /* WailsMenu_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsMenu.m b/v2/internal/frontend/desktop/darwin/WailsMenu.m new file mode 100644 index 000000000..66e5dd399 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsMenu.m @@ -0,0 +1,340 @@ +//go:build darwin +// +// WailsMenu.m +// test +// +// Created by Lea Anthony on 25/10/21. +// + +#import +#import "WailsMenu.h" +#import "WailsMenuItem.h" +#import "Role.h" + +@implementation WailsMenu + +- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags { + NSMenuItem *result = [[[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key] autorelease]; + [result setKeyEquivalentModifierMask:flags]; + return result; +} + +- (NSMenuItem*) newMenuItemWithContext :(WailsContext*)ctx :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags { + NSMenuItem *result = [NSMenuItem new]; + if ( title != nil ) { + [result setTitle:title]; + } + if (selector != nil) { + [result setAction:selector]; + } + if (key) { + [result setKeyEquivalent:key]; + } + if( flags != 0 ) { + [result setKeyEquivalentModifierMask:flags]; + } + result.target = ctx; + return result; +} + +- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key { + return [self newMenuItem :title :selector :key :0]; +} + +- (WailsMenu*) initWithNSTitle:(NSString *)title { + if( title != nil ) { + [super initWithTitle:title]; + } else { + [self init]; + } + [self setAutoenablesItems:NO]; + return self; +} + +- (void) appendSubmenu :(WailsMenu*)child { + NSMenuItem *childMenuItem = [[NSMenuItem new] autorelease]; + [childMenuItem setTitle:child.title]; + [self addItem:childMenuItem]; + [childMenuItem setSubmenu:child]; +} + +- (void) appendRole :(WailsContext*)ctx :(Role)role { + + switch(role) { + case AppMenu: + { + NSString *appName = [NSRunningApplication currentApplication].localizedName; + if( appName == nil ) { + appName = [[NSProcessInfo processInfo] processName]; + } + WailsMenu *appMenu = [[[WailsMenu new] initWithNSTitle:appName] autorelease]; + + if (ctx.aboutTitle != nil) { + [appMenu addItem:[self newMenuItemWithContext :ctx :[@"About " stringByAppendingString:appName] :@selector(About) :nil :0]]; + [appMenu addItem:[NSMenuItem separatorItem]]; + } + + [appMenu addItem:[self newMenuItem:[@"Hide " stringByAppendingString:appName] :@selector(hide:) :@"h" :NSEventModifierFlagCommand]]; + [appMenu addItem:[self newMenuItem:@"Hide Others" :@selector(hideOtherApplications:) :@"h" :(NSEventModifierFlagOption | NSEventModifierFlagCommand)]]; + [appMenu addItem:[self newMenuItem:@"Show All" :@selector(unhideAllApplications:) :@""]]; + [appMenu addItem:[NSMenuItem separatorItem]]; + + id quitTitle = [@"Quit " stringByAppendingString:appName]; + NSMenuItem* quitMenuItem = [self newMenuItem:quitTitle :@selector(Quit) :@"q" :NSEventModifierFlagCommand]; + quitMenuItem.target = ctx; + [appMenu addItem:quitMenuItem]; + [self appendSubmenu:appMenu]; + break; + } + case EditMenu: + { + WailsMenu *editMenu = [[[WailsMenu new] initWithNSTitle:@"Edit"] autorelease]; + [editMenu addItem:[self newMenuItem:@"Undo" :@selector(undo:) :@"z" :NSEventModifierFlagCommand]]; + [editMenu addItem:[self newMenuItem:@"Redo" :@selector(redo:) :@"z" :(NSEventModifierFlagShift | NSEventModifierFlagCommand)]]; + [editMenu addItem:[NSMenuItem separatorItem]]; + [editMenu addItem:[self newMenuItem:@"Cut" :@selector(cut:) :@"x" :NSEventModifierFlagCommand]]; + [editMenu addItem:[self newMenuItem:@"Copy" :@selector(copy:) :@"c" :NSEventModifierFlagCommand]]; + [editMenu addItem:[self newMenuItem:@"Paste" :@selector(paste:) :@"v" :NSEventModifierFlagCommand]]; + [editMenu addItem:[self newMenuItem:@"Paste and Match Style" :@selector(pasteAsRichText:) :@"v" :(NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand)]]; + [editMenu addItem:[self newMenuItem:@"Delete" :@selector(delete:) :[self accel:@"backspace"] :0]]; + [editMenu addItem:[self newMenuItem:@"Select All" :@selector(selectAll:) :@"a" :NSEventModifierFlagCommand]]; + [editMenu addItem:[NSMenuItem separatorItem]]; +// NSMenuItem *speechMenuItem = [[NSMenuItem new] autorelease]; +// [speechMenuItem setTitle:@"Speech"]; +// [editMenu addItem:speechMenuItem]; + WailsMenu *speechMenu = [[[WailsMenu new] initWithNSTitle:@"Speech"] autorelease]; + [speechMenu addItem:[self newMenuItem:@"Start Speaking" :@selector(startSpeaking:) :@""]]; + [speechMenu addItem:[self newMenuItem:@"Stop Speaking" :@selector(stopSpeaking:) :@""]]; + [editMenu appendSubmenu:speechMenu]; + [self appendSubmenu:editMenu]; + + break; + } + case WindowMenu: + { + WailsMenu *windowMenu = [[[WailsMenu new] initWithNSTitle:@"Window"] autorelease]; + [windowMenu addItem:[self newMenuItem:@"Minimize" :@selector(performMiniaturize:) :@"m" :NSEventModifierFlagCommand]]; + [windowMenu addItem:[self newMenuItem:@"Zoom" :@selector(performZoom:) :@""]]; + [windowMenu addItem:[NSMenuItem separatorItem]]; + [windowMenu addItem:[self newMenuItem:@"Full Screen" :@selector(enterFullScreenMode:) :@"f" :(NSEventModifierFlagControl | NSEventModifierFlagCommand)]]; + [self appendSubmenu:windowMenu]; + + break; + } + } +} + +- (void*) AppendMenuItem :(WailsContext*)ctx :(NSString*)label :(NSString *)shortcutKey :(int)modifiers :(bool)disabled :(bool)checked :(int)menuItemID { + + NSString *nslabel = @""; + if (label != nil ) { + nslabel = label; + } + WailsMenuItem *menuItem = [WailsMenuItem new]; + + // Label + menuItem.title = nslabel; + + // Process callback + menuItem.menuItemID = menuItemID; + menuItem.action = @selector(handleClick); + menuItem.target = menuItem; + + // Shortcut + if (shortcutKey != nil) { + [menuItem setKeyEquivalent:[self accel:shortcutKey]]; + [menuItem setKeyEquivalentModifierMask:modifiers]; + } + + // Enabled/Disabled + [menuItem setEnabled:!disabled]; + + // Checked + [menuItem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)]; + + [self addItem:menuItem]; + return menuItem; +} + +- (void) AppendSeparator { + [self addItem:[NSMenuItem separatorItem]]; +} + + +- (NSString*) accel :(NSString*)key { + + // Guard against no accelerator key + if( key == NULL ) { + return @""; + } + + if( [key isEqualToString:@"backspace"] ) { + return unicode(0x0008); + } + if( [key isEqualToString:@"tab"] ) { + return unicode(0x0009); + } + if( [key isEqualToString:@"return"] ) { + return unicode(0x000d); + } + if( [key isEqualToString:@"enter"] ) { + return unicode(0x000d); + } + if( [key isEqualToString:@"escape"] ) { + return unicode(0x001b); + } + if( [key isEqualToString:@"left"] ) { + return unicode(0x001c); + } + if( [key isEqualToString:@"right"] ) { + return unicode(0x001d); + } + if( [key isEqualToString:@"up"] ) { + return unicode(0x001e); + } + if( [key isEqualToString:@"down"] ) { + return unicode(0x001f); + } + if( [key isEqualToString:@"space"] ) { + return unicode(0x0020); + } + if( [key isEqualToString:@"delete"] ) { + return unicode(0x007f); + } + if( [key isEqualToString:@"home"] ) { + return unicode(0x2196); + } + if( [key isEqualToString:@"end"] ) { + return unicode(0x2198); + } + if( [key isEqualToString:@"page up"] ) { + return unicode(0x21de); + } + if( [key isEqualToString:@"page down"] ) { + return unicode(0x21df); + } + if( [key isEqualToString:@"f1"] ) { + return unicode(0xf704); + } + if( [key isEqualToString:@"f2"] ) { + return unicode(0xf705); + } + if( [key isEqualToString:@"f3"] ) { + return unicode(0xf706); + } + if( [key isEqualToString:@"f4"] ) { + return unicode(0xf707); + } + if( [key isEqualToString:@"f5"] ) { + return unicode(0xf708); + } + if( [key isEqualToString:@"f6"] ) { + return unicode(0xf709); + } + if( [key isEqualToString:@"f7"] ) { + return unicode(0xf70a); + } + if( [key isEqualToString:@"f8"] ) { + return unicode(0xf70b); + } + if( [key isEqualToString:@"f9"] ) { + return unicode(0xf70c); + } + if( [key isEqualToString:@"f10"] ) { + return unicode(0xf70d); + } + if( [key isEqualToString:@"f11"] ) { + return unicode(0xf70e); + } + if( [key isEqualToString:@"f12"] ) { + return unicode(0xf70f); + } + if( [key isEqualToString:@"f13"] ) { + return unicode(0xf710); + } + if( [key isEqualToString:@"f14"] ) { + return unicode(0xf711); + } + if( [key isEqualToString:@"f15"] ) { + return unicode(0xf712); + } + if( [key isEqualToString:@"f16"] ) { + return unicode(0xf713); + } + if( [key isEqualToString:@"f17"] ) { + return unicode(0xf714); + } + if( [key isEqualToString:@"f18"] ) { + return unicode(0xf715); + } + if( [key isEqualToString:@"f19"] ) { + return unicode(0xf716); + } + if( [key isEqualToString:@"f20"] ) { + return unicode(0xf717); + } + if( [key isEqualToString:@"f21"] ) { + return unicode(0xf718); + } + if( [key isEqualToString:@"f22"] ) { + return unicode(0xf719); + } + if( [key isEqualToString:@"f23"] ) { + return unicode(0xf71a); + } + if( [key isEqualToString:@"f24"] ) { + return unicode(0xf71b); + } + if( [key isEqualToString:@"f25"] ) { + return unicode(0xf71c); + } + if( [key isEqualToString:@"f26"] ) { + return unicode(0xf71d); + } + if( [key isEqualToString:@"f27"] ) { + return unicode(0xf71e); + } + if( [key isEqualToString:@"f28"] ) { + return unicode(0xf71f); + } + if( [key isEqualToString:@"f29"] ) { + return unicode(0xf720); + } + if( [key isEqualToString:@"f30"] ) { + return unicode(0xf721); + } + if( [key isEqualToString:@"f31"] ) { + return unicode(0xf722); + } + if( [key isEqualToString:@"f32"] ) { + return unicode(0xf723); + } + if( [key isEqualToString:@"f33"] ) { + return unicode(0xf724); + } + if( [key isEqualToString:@"f34"] ) { + return unicode(0xf725); + } + if( [key isEqualToString:@"f35"] ) { + return unicode(0xf726); + } +// if( [key isEqualToString:@"Insert"] ) { +// return unicode(0xf727); +// } +// if( [key isEqualToString:@"PrintScreen"] ) { +// return unicode(0xf72e); +// } +// if( [key isEqualToString:@"ScrollLock"] ) { +// return unicode(0xf72f); +// } + if( [key isEqualToString:@"numLock"] ) { + return unicode(0xf739); + } + + return key; +} + + +@end + + diff --git a/v2/internal/frontend/desktop/darwin/WailsMenuItem.h b/v2/internal/frontend/desktop/darwin/WailsMenuItem.h new file mode 100644 index 000000000..278bac80f --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsMenuItem.h @@ -0,0 +1,22 @@ +// +// WailsMenuItem.h +// test +// +// Created by Lea Anthony on 27/10/21. +// + +#ifndef WailsMenuItem_h +#define WailsMenuItem_h + +#import + +@interface WailsMenuItem : NSMenuItem + +@property int menuItemID; + +- (void) handleClick; + +@end + + +#endif /* WailsMenuItem_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsMenuItem.m b/v2/internal/frontend/desktop/darwin/WailsMenuItem.m new file mode 100644 index 000000000..a34a67239 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsMenuItem.m @@ -0,0 +1,21 @@ +//go:build darwin +// +// WailsMenuItem.m +// test +// +// Created by Lea Anthony on 27/10/21. +// + +#import + +#import "WailsMenuItem.h" +#include "message.h" + + +@implementation WailsMenuItem + +- (void) handleClick { + processCallback(self.menuItemID); +} + +@end diff --git a/v2/internal/frontend/desktop/darwin/WailsWebView.h b/v2/internal/frontend/desktop/darwin/WailsWebView.h new file mode 100644 index 000000000..b6f746cf2 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsWebView.h @@ -0,0 +1,14 @@ +#ifndef WailsWebView_h +#define WailsWebView_h + +#import +#import + +// We will override WKWebView, so we can detect file drop in obj-c +// and grab their file path, to then inject into JS +@interface WailsWebView : WKWebView +@property bool disableWebViewDragAndDrop; +@property bool enableDragAndDrop; +@end + +#endif /* WailsWebView_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsWebView.m b/v2/internal/frontend/desktop/darwin/WailsWebView.m new file mode 100644 index 000000000..de23ac794 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsWebView.m @@ -0,0 +1,122 @@ +#import "WailsWebView.h" +#import "message.h" + + +@implementation WailsWebView +@synthesize disableWebViewDragAndDrop; +@synthesize enableDragAndDrop; + +- (BOOL)prepareForDragOperation:(id)sender +{ + if ( !enableDragAndDrop ) { + return [super prepareForDragOperation: sender]; + } + + if ( disableWebViewDragAndDrop ) { + return YES; + } + + return [super prepareForDragOperation: sender]; +} + +- (BOOL)performDragOperation:(id )sender +{ + if ( !enableDragAndDrop ) { + return [super performDragOperation: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) + return [super performDragOperation: sender]; + + // getting all NSURL types + NSArray *url_class = @[[NSURL class]]; + NSDictionary *options = @{}; + NSArray *files = [pboard readObjectsForClasses:url_class options:options]; + + // collecting all file paths + NSMutableArray *files_strs = [[NSMutableArray alloc] init]; + for (NSURL *url in files) + { + const char *fs_path = [url fileSystemRepresentation]; //Will be UTF-8 encoded + NSString *fs_path_str = [[NSString alloc] initWithCString:fs_path encoding:NSUTF8StringEncoding]; + [files_strs addObject:fs_path_str]; +// NSLog( @"performDragOperation: file path: %s", fs_path ); + } + + NSString *joined=[files_strs componentsJoinedByString:@"\n"]; + + // Release the array of file paths + [files_strs release]; + + int dragXLocation = [sender draggingLocation].x - [self frame].origin.x; + int dragYLocation = [self frame].size.height - [sender draggingLocation].y; // Y coordinate is inverted, so we need to subtract from the height + +// NSLog( @"draggingUpdated: X coord: %d", dragXLocation ); +// NSLog( @"draggingUpdated: Y coord: %d", dragYLocation ); + + NSString *message = [NSString stringWithFormat:@"DD:%d:%d:%@", dragXLocation, dragYLocation, joined]; + + const char* res = message.UTF8String; + + processMessage(res); + + if ( disableWebViewDragAndDrop ) { + return YES; + } + + return [super performDragOperation: sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender { + if ( !enableDragAndDrop ) { + return [super draggingUpdated: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) { + return [super draggingUpdated: sender]; + } + + if ( disableWebViewDragAndDrop ) { + // we should call supper as otherwise events will not pass + [super draggingUpdated: sender]; + + // pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage + return 4; + } + + return [super draggingUpdated: sender]; +} + +- (NSDragOperation)draggingEntered:(id )sender { + if ( !enableDragAndDrop ) { + return [super draggingEntered: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) { + return [super draggingEntered: sender]; + } + + if ( disableWebViewDragAndDrop ) { + // we should call supper as otherwise events will not pass + [super draggingEntered: sender]; + + // pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage + return 4; + } + + return [super draggingEntered: sender]; +} + +@end diff --git a/v2/internal/frontend/desktop/darwin/WindowDelegate.h b/v2/internal/frontend/desktop/darwin/WindowDelegate.h new file mode 100644 index 000000000..6f83e0e48 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WindowDelegate.h @@ -0,0 +1,25 @@ +// +// WindowDelegate.h +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#ifndef WindowDelegate_h +#define WindowDelegate_h + +#import "WailsContext.h" + +@interface WindowDelegate : NSObject + +@property bool hideOnClose; + +@property (assign) WailsContext* ctx; + +- (void)windowDidExitFullScreen:(NSNotification *)notification; + + +@end + + +#endif /* WindowDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/WindowDelegate.m b/v2/internal/frontend/desktop/darwin/WindowDelegate.m new file mode 100644 index 000000000..915f12853 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WindowDelegate.m @@ -0,0 +1,38 @@ +//go:build darwin +// +// WindowDelegate.m +// test +// +// Created by Lea Anthony on 10/10/21. +// + +#import +#import +#import "WindowDelegate.h" +#import "message.h" +#import "WailsContext.h" + +@implementation WindowDelegate +- (BOOL)windowShouldClose:(WailsWindow *)sender { + if( self.hideOnClose ) { + [NSApp hide:nil]; + return false; + } + processMessage("Q"); + return false; +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + [self.ctx.mainWindow applyWindowConstraints]; +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification { + [self.ctx.mainWindow disableWindowConstraints]; +} + +- (NSApplicationPresentationOptions)window:(WailsWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions { + return NSApplicationPresentationAutoHideToolbar | NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationFullScreen; +} + + +@end diff --git a/v2/internal/frontend/desktop/darwin/browser.go b/v2/internal/frontend/desktop/darwin/browser.go new file mode 100644 index 000000000..c865ab6d9 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/browser.go @@ -0,0 +1,24 @@ +//go:build darwin +// +build darwin + +package darwin + +import ( + "fmt" + "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" +) + +// BrowserOpenURL Use the default browser to open the url +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } + + // Specific method implementation + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } +} diff --git a/v2/internal/frontend/desktop/darwin/callbacks.go b/v2/internal/frontend/desktop/darwin/callbacks.go new file mode 100644 index 000000000..ab0d18e47 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/callbacks.go @@ -0,0 +1,51 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" + +#include +*/ +import "C" + +import ( + "errors" + "strconv" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +func (f *Frontend) handleCallback(menuItemID uint) error { + menuItem := getMenuItemForID(menuItemID) + if menuItem == nil { + return errors.New("unknown menuItem ID: " + strconv.Itoa(int(menuItemID))) + } + + wailsMenuItem := menuItem.wailsMenuItem + if wailsMenuItem.Type == menu.CheckboxType { + wailsMenuItem.Checked = !wailsMenuItem.Checked + C.UpdateMenuItem(menuItem.nsmenuitem, bool2Cint(wailsMenuItem.Checked)) + } + if wailsMenuItem.Type == menu.RadioType { + // Ignore if we clicked the item that is already checked + if !wailsMenuItem.Checked { + for _, item := range menuItem.radioGroupMembers { + if item.wailsMenuItem.Checked { + item.wailsMenuItem.Checked = false + C.UpdateMenuItem(item.nsmenuitem, C.int(0)) + } + } + wailsMenuItem.Checked = true + C.UpdateMenuItem(menuItem.nsmenuitem, C.int(1)) + } + } + if wailsMenuItem.Click != nil { + go wailsMenuItem.Click(&menu.CallbackData{MenuItem: wailsMenuItem}) + } + return nil +} diff --git a/v2/internal/frontend/desktop/darwin/calloc.go b/v2/internal/frontend/desktop/darwin/calloc.go new file mode 100644 index 000000000..afd0a9115 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/calloc.go @@ -0,0 +1,34 @@ +//go:build darwin + +package darwin + +/* +#include +*/ +import "C" +import "unsafe" + +// Calloc handles alloc/dealloc of C data +type Calloc struct { + pool []unsafe.Pointer +} + +// NewCalloc creates a new allocator +func NewCalloc() Calloc { + return Calloc{} +} + +// String creates a new C string and retains a reference to it +func (c Calloc) String(in string) *C.char { + result := C.CString(in) + c.pool = append(c.pool, unsafe.Pointer(result)) + return result +} + +// Free frees all allocated C memory +func (c Calloc) Free() { + for _, str := range c.pool { + C.free(str) + } + c.pool = []unsafe.Pointer{} +} diff --git a/v2/internal/frontend/desktop/darwin/clipboard.go b/v2/internal/frontend/desktop/darwin/clipboard.go new file mode 100644 index 000000000..c40ba8771 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/clipboard.go @@ -0,0 +1,35 @@ +//go:build darwin + +package darwin + +import ( + "os/exec" +) + +func (f *Frontend) ClipboardGetText() (string, error) { + pasteCmd := exec.Command("pbpaste") + out, err := pasteCmd.Output() + if err != nil { + return "", err + } + return string(out), nil +} + +func (f *Frontend) ClipboardSetText(text string) error { + copyCmd := exec.Command("pbcopy") + in, err := copyCmd.StdinPipe() + if err != nil { + return err + } + + if err := copyCmd.Start(); err != nil { + return err + } + if _, err := in.Write([]byte(text)); err != nil { + return err + } + if err := in.Close(); err != nil { + return err + } + return copyCmd.Wait() +} diff --git a/v2/internal/frontend/desktop/darwin/dialog.go b/v2/internal/frontend/desktop/darwin/dialog.go new file mode 100644 index 000000000..66bb2f13a --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/dialog.go @@ -0,0 +1,196 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" +*/ +import "C" + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + "unsafe" + + "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/frontend" +) + +// Obj-C dialog methods send the response to this channel +var ( + messageDialogResponse = make(chan int) + openFileDialogResponse = make(chan string) + saveFileDialogResponse = make(chan string) + dialogLock sync.Mutex +) + +// OpenDirectoryDialog prompts the user to select a directory +func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (string, error) { + results, err := f.openDialog(&options, false, false, true) + if err != nil { + return "", err + } + var selected string + if len(results) > 0 { + selected = results[0] + } + return selected, nil +} + +func (f *Frontend) openDialog(options *frontend.OpenDialogOptions, multiple bool, allowfiles bool, allowdirectories bool) ([]string, error) { + dialogLock.Lock() + defer dialogLock.Unlock() + + c := NewCalloc() + defer c.Free() + title := c.String(options.Title) + defaultFilename := c.String(options.DefaultFilename) + defaultDirectory := c.String(options.DefaultDirectory) + allowDirectories := bool2Cint(allowdirectories) + allowFiles := bool2Cint(allowfiles) + canCreateDirectories := bool2Cint(options.CanCreateDirectories) + treatPackagesAsDirectories := bool2Cint(options.TreatPackagesAsDirectories) + resolveAliases := bool2Cint(options.ResolvesAliases) + showHiddenFiles := bool2Cint(options.ShowHiddenFiles) + allowMultipleFileSelection := bool2Cint(multiple) + + var filterStrings slicer.StringSlicer + if options.Filters != nil { + for _, filter := range options.Filters { + thesePatterns := strings.Split(filter.Pattern, ";") + for _, pattern := range thesePatterns { + pattern = strings.TrimSpace(pattern) + if pattern != "" { + filterStrings.Add(pattern) + } + } + } + filterStrings.Deduplicate() + } + filters := filterStrings.Join(";") + C.OpenFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, allowDirectories, allowFiles, canCreateDirectories, treatPackagesAsDirectories, resolveAliases, showHiddenFiles, allowMultipleFileSelection, c.String(filters)) + + result := <-openFileDialogResponse + + var parsedResults []string + err := json.Unmarshal([]byte(result), &parsedResults) + + return parsedResults, err +} + +// OpenFileDialog prompts the user to select a file +func (f *Frontend) OpenFileDialog(options frontend.OpenDialogOptions) (string, error) { + results, err := f.openDialog(&options, false, true, false) + if err != nil { + return "", err + } + var selected string + if len(results) > 0 { + selected = results[0] + } + return selected, nil +} + +// OpenMultipleFilesDialog prompts the user to select a file +func (f *Frontend) OpenMultipleFilesDialog(options frontend.OpenDialogOptions) ([]string, error) { + return f.openDialog(&options, true, true, false) +} + +// SaveFileDialog prompts the user to select a file +func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, error) { + dialogLock.Lock() + defer dialogLock.Unlock() + + c := NewCalloc() + defer c.Free() + title := c.String(options.Title) + defaultFilename := c.String(options.DefaultFilename) + defaultDirectory := c.String(options.DefaultDirectory) + canCreateDirectories := bool2Cint(options.CanCreateDirectories) + treatPackagesAsDirectories := bool2Cint(options.TreatPackagesAsDirectories) + showHiddenFiles := bool2Cint(options.ShowHiddenFiles) + + var filterStrings slicer.StringSlicer + if options.Filters != nil { + for _, filter := range options.Filters { + thesePatterns := strings.Split(filter.Pattern, ";") + for _, pattern := range thesePatterns { + pattern = strings.TrimSpace(pattern) + if pattern != "" { + filterStrings.Add(pattern) + } + } + } + filterStrings.Deduplicate() + } + filters := filterStrings.Join(";") + C.SaveFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, canCreateDirectories, treatPackagesAsDirectories, showHiddenFiles, c.String(filters)) + + result := <-saveFileDialogResponse + + return result, nil +} + +// MessageDialog show a message dialog to the user +func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, error) { + dialogLock.Lock() + defer dialogLock.Unlock() + + c := NewCalloc() + defer c.Free() + dialogType := c.String(string(options.Type)) + title := c.String(options.Title) + message := c.String(options.Message) + defaultButton := c.String(options.DefaultButton) + cancelButton := c.String(options.CancelButton) + const MaxButtons = 4 + var buttons [MaxButtons]*C.char + for index, buttonText := range options.Buttons { + if index == MaxButtons { + return "", fmt.Errorf("max %d buttons supported (%d given)", MaxButtons, len(options.Buttons)) + } + buttons[index] = c.String(buttonText) + } + + var iconData unsafe.Pointer + var iconDataLength C.int + if options.Icon != nil { + iconData = unsafe.Pointer(&options.Icon[0]) + iconDataLength = C.int(len(options.Icon)) + } + + C.MessageDialog(f.mainWindow.context, dialogType, title, message, buttons[0], buttons[1], buttons[2], buttons[3], defaultButton, cancelButton, iconData, iconDataLength) + + result := <-messageDialogResponse + + selectedC := buttons[result] + var selected string + if selectedC != nil { + selected = options.Buttons[result] + } + return selected, nil +} + +//export processMessageDialogResponse +func processMessageDialogResponse(selection int) { + messageDialogResponse <- selection +} + +//export processOpenFileDialogResponse +func processOpenFileDialogResponse(cselection *C.char) { + selection := C.GoString(cselection) + openFileDialogResponse <- selection +} + +//export processSaveFileDialogResponse +func processSaveFileDialogResponse(cselection *C.char) { + selection := C.GoString(cselection) + saveFileDialogResponse <- selection +} diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go new file mode 100644 index 000000000..6566445d5 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -0,0 +1,525 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "CustomProtocol.h" +#import "WailsContext.h" + +#include +*/ +import "C" + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "log" + "net" + "net/url" + "os" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/assetserver" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" + "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +const startURL = "wails://wails/" + +type bindingsMessage struct { + message string + source string + isMainFrame bool +} + +var ( + messageBuffer = make(chan string, 100) + bindingsMessageBuffer = make(chan *bindingsMessage, 100) + requestBuffer = make(chan webview.Request, 100) + callbackBuffer = make(chan uint, 10) + openFilepathBuffer = make(chan string, 100) + openUrlBuffer = make(chan string, 100) + secondInstanceBuffer = make(chan options.SecondInstanceData, 1) +) + +type Frontend struct { + // Context + ctx context.Context + + frontendOptions *options.App + logger *logger.Logger + debug bool + devtoolsEnabled bool + + // Keep single instance lock file, so that it will not be GC and lock will exist while app is running + singleInstanceLockFile *os.File + + // Assets + assets *assetserver.AssetServer + startURL *url.URL + + // main window handle + mainWindow *Window + bindings *binding.Bindings + dispatcher frontend.Dispatcher + + originValidator *originvalidator.OriginValidator +} + +func (f *Frontend) RunMainLoop() { + C.RunMainLoop() +} + +func (f *Frontend) WindowClose() { + C.ReleaseContext(f.mainWindow.context) +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + result := &Frontend{ + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + } + result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + + // this should be initialized as early as possible to handle first instance launch + C.StartCustomProtocolHandler() + + if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { + result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + } else { + if port, _ := ctx.Value("assetserverport").(string); port != "" { + result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + } + + var bindings string + var err error + if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated { + bindings, err = appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + } else { + appBindings.DB().UpdateObfuscatedCallMap() + } + + assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, runtime.RuntimeAssetsBundle) + if err != nil { + log.Fatal(err) + } + assets.ExpectedWebViewHost = result.startURL.Host + result.assets = assets + + go result.startRequestProcessor() + } + + go result.startMessageProcessor() + go result.startBindingsMessageProcessor() + go result.startCallbackProcessor() + go result.startFileOpenProcessor() + go result.startUrlOpenProcessor() + go result.startSecondInstanceProcessor() + + return result +} + +func (f *Frontend) startFileOpenProcessor() { + for filePath := range openFilepathBuffer { + f.ProcessOpenFileEvent(filePath) + } +} + +func (f *Frontend) startUrlOpenProcessor() { + for url := range openUrlBuffer { + f.ProcessOpenUrlEvent(url) + } +} + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} + +func (f *Frontend) startMessageProcessor() { + for message := range messageBuffer { + f.processMessage(message) + } +} + +func (f *Frontend) startBindingsMessageProcessor() { + for msg := range bindingsMessageBuffer { + // Apple webkit doesn't provide origin of main frame. So we can't verify in case of iFrame that top level origin is allowed. + if !msg.isMainFrame { + f.logger.Error("Blocked request from not main frame") + continue + } + + origin, err := f.originValidator.GetOriginFromURL(msg.source) + if err != nil { + f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err)) + continue + } + + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + continue + } + + f.processMessage(msg.message) + } +} + +func (f *Frontend) startRequestProcessor() { + for request := range requestBuffer { + f.assets.ServeWebViewRequest(request) + } +} + +func (f *Frontend) startCallbackProcessor() { + for callback := range callbackBuffer { + err := f.handleCallback(callback) + if err != nil { + println(err.Error()) + } + } +} + +func (f *Frontend) WindowReload() { + f.ExecJS("runtime.WindowReload();") +} + +func (f *Frontend) WindowReloadApp() { + f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL)) +} + +func (f *Frontend) WindowSetSystemDefaultTheme() { +} + +func (f *Frontend) WindowSetLightTheme() { +} + +func (f *Frontend) WindowSetDarkTheme() { +} + +func (f *Frontend) Run(ctx context.Context) error { + f.ctx = ctx + + if f.frontendOptions.SingleInstanceLock != nil { + f.singleInstanceLockFile = SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + + _debug := ctx.Value("debug") + _devtoolsEnabled := ctx.Value("devtoolsEnabled") + + if _debug != nil { + f.debug = _debug.(bool) + } + if _devtoolsEnabled != nil { + f.devtoolsEnabled = _devtoolsEnabled.(bool) + } + + mainWindow := NewWindow(f.frontendOptions, f.debug, f.devtoolsEnabled) + f.mainWindow = mainWindow + f.mainWindow.Center() + + go func() { + if f.frontendOptions.OnStartup != nil { + f.frontendOptions.OnStartup(f.ctx) + } + }() + mainWindow.Run(f.startURL.String()) + return nil +} + +func (f *Frontend) WindowCenter() { + f.mainWindow.Center() +} + +func (f *Frontend) WindowSetAlwaysOnTop(onTop bool) { + f.mainWindow.SetAlwaysOnTop(onTop) +} + +func (f *Frontend) WindowSetPosition(x, y int) { + f.mainWindow.SetPosition(x, y) +} + +func (f *Frontend) WindowGetPosition() (int, int) { + return f.mainWindow.GetPosition() +} + +func (f *Frontend) WindowSetSize(width, height int) { + f.mainWindow.SetSize(width, height) +} + +func (f *Frontend) WindowGetSize() (int, int) { + return f.mainWindow.Size() +} + +func (f *Frontend) WindowSetTitle(title string) { + f.mainWindow.SetTitle(title) +} + +func (f *Frontend) WindowFullscreen() { + f.mainWindow.Fullscreen() +} + +func (f *Frontend) WindowUnfullscreen() { + f.mainWindow.UnFullscreen() +} + +func (f *Frontend) WindowShow() { + f.mainWindow.Show() +} + +func (f *Frontend) WindowHide() { + f.mainWindow.Hide() +} + +func (f *Frontend) Show() { + f.mainWindow.ShowApplication() +} + +func (f *Frontend) Hide() { + f.mainWindow.HideApplication() +} + +func (f *Frontend) WindowMaximise() { + f.mainWindow.Maximise() +} + +func (f *Frontend) WindowToggleMaximise() { + f.mainWindow.ToggleMaximise() +} + +func (f *Frontend) WindowUnmaximise() { + f.mainWindow.UnMaximise() +} + +func (f *Frontend) WindowMinimise() { + f.mainWindow.Minimise() +} + +func (f *Frontend) WindowUnminimise() { + f.mainWindow.UnMinimise() +} + +func (f *Frontend) WindowSetMinSize(width int, height int) { + f.mainWindow.SetMinSize(width, height) +} + +func (f *Frontend) WindowSetMaxSize(width int, height int) { + f.mainWindow.SetMaxSize(width, height) +} + +func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) { + if col == nil { + return + } + f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A) +} + +func (f *Frontend) ScreenGetAll() ([]frontend.Screen, error) { + return GetAllScreens(f.mainWindow.context) +} + +func (f *Frontend) WindowIsMaximised() bool { + return f.mainWindow.IsMaximised() +} + +func (f *Frontend) WindowIsMinimised() bool { + return f.mainWindow.IsMinimised() +} + +func (f *Frontend) WindowIsNormal() bool { + return f.mainWindow.IsNormal() +} + +func (f *Frontend) WindowIsFullscreen() bool { + return f.mainWindow.IsFullScreen() +} + +func (f *Frontend) Quit() { + if f.frontendOptions.OnBeforeClose != nil { + go func() { + if !f.frontendOptions.OnBeforeClose(f.ctx) { + f.mainWindow.Quit() + } + }() + return + } + f.mainWindow.Quit() +} + +func (f *Frontend) WindowPrint() { + f.mainWindow.Print() +} + +type EventNotify struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (f *Frontend) Notify(name string, data ...interface{}) { + notification := EventNotify{ + Name: name, + Data: data, + } + payload, err := json.Marshal(notification) + if err != nil { + f.logger.Error(err.Error()) + return + } + f.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`) +} + +func (f *Frontend) processMessage(message string) { + if message == "DomReady" { + if f.frontendOptions.OnDomReady != nil { + f.frontendOptions.OnDomReady(f.ctx) + } + return + } + + if message == "runtime:ready" { + cmd := fmt.Sprintf("window.wails.setCSSDragProperties('%s', '%s');", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue) + f.ExecJS(cmd) + + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + + return + } + + if message == "wails:openInspector" { + showInspector(f.mainWindow.context) + return + } + + //if strings.HasPrefix(message, "systemevent:") { + // f.processSystemEvent(message) + // return + //} + + go func() { + result, err := f.dispatcher.ProcessMessage(message, f) + if err != nil { + f.logger.Error(err.Error()) + f.Callback(result) + return + } + if result == "" { + return + } + + switch result[0] { + case 'c': + // Callback from a method call + f.Callback(result[1:]) + default: + f.logger.Info("Unknown message returned from dispatcher: %+v", result) + } + }() +} + +func (f *Frontend) ProcessOpenFileEvent(filePath string) { + if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnFileOpen != nil { + f.frontendOptions.Mac.OnFileOpen(filePath) + } +} + +func (f *Frontend) ProcessOpenUrlEvent(url string) { + if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnUrlOpen != nil { + f.frontendOptions.Mac.OnUrlOpen(url) + } +} + +func (f *Frontend) Callback(message string) { + escaped, err := json.Marshal(message) + if err != nil { + panic(err) + } + f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`) +} + +func (f *Frontend) ExecJS(js string) { + f.mainWindow.ExecJS(js) +} + +//func (f *Frontend) processSystemEvent(message string) { +// sl := strings.Split(message, ":") +// if len(sl) != 2 { +// f.logger.Error("Invalid system message: %s", message) +// return +// } +// switch sl[1] { +// case "fullscreen": +// f.mainWindow.DisableSizeConstraints() +// case "unfullscreen": +// f.mainWindow.EnableSizeConstraints() +// default: +// f.logger.Error("Unknown system message: %s", message) +// } +//} + +//export processMessage +func processMessage(message *C.char) { + goMessage := C.GoString(message) + messageBuffer <- goMessage +} + +//export processBindingMessage +func processBindingMessage(message *C.char, source *C.char, fromMainFrame bool) { + goMessage := C.GoString(message) + goSource := C.GoString(source) + bindingsMessageBuffer <- &bindingsMessage{ + message: goMessage, + source: goSource, + isMainFrame: fromMainFrame, + } +} + +//export processCallback +func processCallback(callbackID uint) { + callbackBuffer <- callbackID +} + +//export processURLRequest +func processURLRequest(_ unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) { + requestBuffer <- webview.NewRequest(wkURLSchemeTask) +} + +//export HandleOpenFile +func HandleOpenFile(filePath *C.char) { + goFilepath := C.GoString(filePath) + openFilepathBuffer <- goFilepath +} + +//export HandleOpenURL +func HandleOpenURL(url *C.char) { + goUrl := C.GoString(url) + openUrlBuffer <- goUrl +} diff --git a/v2/internal/frontend/desktop/darwin/inspector.go b/v2/internal/frontend/desktop/darwin/inspector.go new file mode 100644 index 000000000..dc3f08969 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/inspector.go @@ -0,0 +1,10 @@ +//go:build darwin && !(dev || debug || devtools) + +package darwin + +import ( + "unsafe" +) + +func showInspector(_ unsafe.Pointer) { +} diff --git a/v2/internal/frontend/desktop/darwin/inspector_dev.go b/v2/internal/frontend/desktop/darwin/inspector_dev.go new file mode 100644 index 000000000..e79b9c3e7 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/inspector_dev.go @@ -0,0 +1,78 @@ +//go:build darwin && (dev || debug || devtools) + +package darwin + +// We are using private APIs here, make sure this is only included in a dev/debug build and not in a production build. +// Otherwise the binary might get rejected by the AppReview-Team when pushing it to the AppStore. + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "WailsContext.h" + +extern void processMessage(const char *message); + +@interface _WKInspector : NSObject +- (void)show; +- (void)detach; +@end + +@interface WKWebView () +- (_WKInspector *)_inspector; +@end + +void showInspector(void *inctx) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120000 + ON_MAIN_THREAD( + if (@available(macOS 12.0, *)) { + WailsContext *ctx = (__bridge WailsContext*) inctx; + + @try { + [ctx.webview._inspector show]; + } @catch (NSException *exception) { + NSLog(@"Opening the inspector failed: %@", exception.reason); + return; + } + + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + // Detach must be deferred a little bit and is ignored directly after a show. + @try { + [ctx.webview._inspector detach]; + } @catch (NSException *exception) { + NSLog(@"Detaching the inspector failed: %@", exception.reason); + } + }); + } else { + NSLog(@"Opening the inspector needs at least MacOS 12"); + } + ); +#endif +} + +void setupF12hotkey() { + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + if (event.keyCode == 111 && + event.modifierFlags & NSEventModifierFlagFunction && + event.modifierFlags & NSEventModifierFlagCommand && + event.modifierFlags & NSEventModifierFlagShift) { + processMessage("wails:openInspector"); + return nil; + } + return event; + }]; +} +*/ +import "C" +import ( + "unsafe" +) + +func init() { + C.setupF12hotkey() +} + +func showInspector(context unsafe.Pointer) { + C.showInspector(context) +} diff --git a/v2/internal/frontend/desktop/darwin/main.m b/v2/internal/frontend/desktop/darwin/main.m new file mode 100644 index 000000000..75a84dc76 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/main.m @@ -0,0 +1,243 @@ +//go:build ignore +// main.m +// test +// +// Created by Lea Anthony on 10/10/21. +// + +// ****** This file is used for testing purposes only ****** + +#import +#import "Application.h" + +void processMessage(const char*t) { + NSLog(@"processMessage called"); +} + +void processMessageDialogResponse(int t) { + NSLog(@"processMessage called"); +} + +void processOpenFileDialogResponse(const char *t) { + NSLog(@"processMessage called %s", t); +} +void processSaveFileDialogResponse(const char *t) { + NSLog(@"processMessage called %s", t); +} + +void processCallback(int callbackID) { + NSLog(@"Process callback %d", callbackID); +} + +void processURLRequest(void *ctx, unsigned long long requestId, const char* url, const char *method, const char *headers, const void *body, int bodyLen) { + NSLog(@"processURLRequest called"); + const char myByteArray[] = { 0x3c,0x68,0x31,0x3e,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x3c,0x2f,0x68,0x31,0x3e }; + // void *inctx, const char *url, int statusCode, const char *headers, void* data, int datalength + ProcessURLResponse(ctx, requestId, 200, "{\"Content-Type\": \"text/html\"}", (void*)myByteArray, 21); +} + +unsigned char _Users_username_Pictures_SaltBae_png[] = { + +0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, +0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, +0x08, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x89, 0x1d, 0x0d, 0x00, 0x00, 0x00, +0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, +0x05, 0x00, 0x00, 0x00, 0x20, 0x63, 0x48, 0x52, 0x4d, 0x00, 0x00, 0x7a, +0x26, 0x00, 0x00, 0x80, 0x84, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x80, +0xe8, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0xea, 0x60, 0x00, 0x00, 0x3a, +0x98, 0x00, 0x00, 0x17, 0x70, 0x9c, 0xba, 0x51, 0x3c, 0x00, 0x00, 0x00, +0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, +0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x01, 0xd5, 0x69, 0x54, +0x58, 0x74, 0x58, 0x4d, 0x4c, 0x3a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64, +0x6f, 0x62, 0x65, 0x2e, 0x78, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, +0x3c, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, +0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, 0x62, +0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20, +0x78, 0x3a, 0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x58, 0x4d, 0x50, +0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x35, 0x2e, 0x34, 0x2e, 0x30, 0x22, +0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, +0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, +0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, +0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, +0x30, 0x32, 0x2f, 0x32, 0x32, 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, +0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, 0x22, 0x3e, 0x0a, 0x20, +0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, +0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x64, +0x66, 0x3a, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, 0x0a, 0x20, +0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x78, +0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x74, 0x69, 0x66, 0x66, 0x3d, 0x22, 0x68, +0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, +0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x69, 0x66, 0x66, 0x2f, +0x31, 0x2e, 0x30, 0x2f, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, +0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x43, 0x6f, +0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x3e, 0x31, 0x3c, +0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, +0x73, 0x73, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, +0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x4f, 0x72, +0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x31, 0x3c, +0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, +0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, +0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x50, 0x68, +0x6f, 0x74, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x49, 0x6e, 0x74, +0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, +0x32, 0x3c, 0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x50, 0x68, 0x6f, 0x74, +0x6f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, +0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, +0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, +0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, +0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, +0x3e, 0x0a, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, +0x61, 0x3e, 0x0a, 0x02, 0xd8, 0x80, 0x05, 0x00, 0x00, 0x04, 0xdc, 0x49, +0x44, 0x41, 0x54, 0x38, 0x11, 0x1d, 0x94, 0x49, 0x6c, 0x1b, 0x65, 0x18, +0x86, 0x9f, 0x99, 0xf9, 0x67, 0xc6, 0x6b, 0xbc, 0x26, 0xce, 0xda, 0xa4, +0x25, 0x69, 0x0b, 0x2d, 0x28, 0x34, 0x2c, 0x95, 0x00, 0x89, 0x45, 0x08, +0x5a, 0x95, 0x03, 0x08, 0x09, 0x21, 0xe0, 0x80, 0x38, 0xc3, 0x85, 0x03, +0xe2, 0x00, 0x47, 0xc4, 0x1d, 0x38, 0x70, 0xe3, 0xc6, 0x01, 0x01, 0x42, +0x20, 0x54, 0x7a, 0x2a, 0x6b, 0x0b, 0x94, 0xd2, 0xd2, 0x25, 0x69, 0x9b, +0xa4, 0x0d, 0x2d, 0xa9, 0xb3, 0x78, 0x89, 0x9d, 0xf1, 0x2c, 0x9e, 0x85, +0x2f, 0xb5, 0x35, 0xb6, 0x35, 0x96, 0xde, 0x79, 0xdf, 0xef, 0x7f, 0x9f, +0x4f, 0xfb, 0xe0, 0xad, 0x37, 0x12, 0xfd, 0xf0, 0xb3, 0x9c, 0xfb, 0xb7, +0xc5, 0x8d, 0x46, 0x9b, 0x71, 0x5b, 0xf1, 0xd0, 0xf4, 0x18, 0xdb, 0xeb, +0x4b, 0x1c, 0xff, 0xf1, 0x57, 0x98, 0xdc, 0x87, 0x72, 0x3a, 0x8c, 0x3a, +0xcb, 0x8c, 0xea, 0x31, 0x35, 0xb7, 0xc3, 0x99, 0xba, 0xc3, 0xd7, 0xab, +0x3e, 0x87, 0x2a, 0x8a, 0xb3, 0xff, 0xdc, 0xe0, 0x9b, 0x8f, 0x5f, 0xa2, +0x1c, 0xc5, 0xfc, 0x72, 0xc9, 0x41, 0x99, 0x71, 0x48, 0xca, 0x84, 0x3c, +0x3e, 0xda, 0xd2, 0x05, 0x9a, 0xb1, 0xc7, 0x35, 0x67, 0x1c, 0xdd, 0x4c, +0x68, 0xeb, 0x26, 0xd9, 0x30, 0x26, 0x09, 0x23, 0x5c, 0x3f, 0xc2, 0xd3, +0x43, 0xc2, 0x24, 0x21, 0x4e, 0x34, 0x40, 0x27, 0x89, 0x13, 0xf9, 0x1e, +0x22, 0x6e, 0xd5, 0x45, 0x43, 0x63, 0xc6, 0xd2, 0x50, 0xa9, 0xc4, 0x67, +0x24, 0x15, 0x72, 0xa9, 0x7e, 0x95, 0xfa, 0x4f, 0x27, 0x78, 0x64, 0x76, +0x86, 0x23, 0x61, 0xc0, 0xf0, 0x58, 0x15, 0xc3, 0x29, 0x71, 0x06, 0x45, +0x2e, 0xa5, 0x48, 0xbb, 0x0a, 0x3d, 0x89, 0xa0, 0x8f, 0x08, 0x8a, 0x8e, +0x08, 0xbb, 0xc1, 0x8e, 0xb0, 0x8d, 0xdd, 0x0f, 0xc9, 0x84, 0x06, 0x65, +0x34, 0xf4, 0xed, 0x8d, 0xff, 0x58, 0xbd, 0xfc, 0x27, 0x17, 0x2f, 0x9e, +0xe3, 0xf0, 0x81, 0x49, 0x5e, 0xde, 0x5f, 0xe1, 0x9e, 0x82, 0xcd, 0xdc, +0x78, 0x8d, 0xd9, 0xb2, 0xc9, 0x56, 0x12, 0x32, 0x94, 0x4f, 0x91, 0xcb, +0x88, 0x68, 0xda, 0x42, 0x13, 0x77, 0x11, 0xa2, 0xa8, 0xc3, 0x5a, 0x5f, +0x46, 0x30, 0x65, 0x52, 0x29, 0xe4, 0x24, 0x4d, 0x8e, 0xcc, 0x68, 0x19, +0xe5, 0x76, 0xbb, 0xac, 0x5c, 0x98, 0xa7, 0xb3, 0xed, 0xd0, 0x37, 0x62, +0xa2, 0xb0, 0xc7, 0x89, 0xe5, 0x2e, 0x03, 0x0d, 0x97, 0x95, 0x46, 0x8f, +0x31, 0xd7, 0xa6, 0x63, 0x81, 0x65, 0x25, 0x84, 0xba, 0x45, 0x5f, 0x65, +0x31, 0x2c, 0x71, 0x6b, 0x77, 0x69, 0xf5, 0x7a, 0xbc, 0xb0, 0x3b, 0xcd, +0xf9, 0xa5, 0x90, 0xd1, 0xb0, 0xcd, 0xd4, 0xb0, 0xdc, 0xd7, 0xc4, 0xfa, +0xf0, 0x78, 0x95, 0x7b, 0x27, 0xab, 0x5c, 0x5e, 0x6e, 0xd2, 0xee, 0x05, +0xdc, 0xd8, 0xea, 0xf1, 0xf7, 0xe2, 0x1a, 0xc7, 0xee, 0x1a, 0x62, 0x2e, +0x1f, 0xe3, 0xe8, 0xb6, 0xc4, 0x4c, 0xd3, 0x6d, 0x6e, 0xd0, 0x6b, 0xfc, +0x4c, 0xe3, 0xd4, 0x1f, 0xc4, 0x4b, 0xf3, 0x1c, 0x2c, 0x65, 0x29, 0x67, +0x4d, 0xbe, 0xfb, 0xad, 0x45, 0x65, 0x0c, 0xea, 0x7e, 0x1f, 0x15, 0x6b, +0x09, 0x0b, 0x8b, 0xb7, 0x19, 0xc9, 0xa5, 0x78, 0x75, 0x6e, 0x18, 0xdf, +0xf5, 0x79, 0x72, 0xd0, 0xa2, 0x2d, 0xb3, 0x3a, 0xbb, 0xb4, 0x41, 0x3e, +0x53, 0xe6, 0xf4, 0xca, 0x3c, 0xa5, 0x7c, 0x86, 0xe9, 0xfd, 0x47, 0x18, +0x2e, 0xbd, 0xce, 0xd1, 0x97, 0x26, 0x78, 0xbc, 0x7e, 0x1d, 0xff, 0xcc, +0xa7, 0x5c, 0x71, 0x74, 0x16, 0xe3, 0x18, 0xd7, 0x1e, 0x23, 0xe8, 0xac, +0xa3, 0x0c, 0xcd, 0x60, 0x22, 0x6f, 0x43, 0x36, 0x43, 0x3b, 0x19, 0xc6, +0x08, 0x7a, 0xe0, 0x6c, 0xe3, 0x27, 0x8a, 0xdb, 0x4e, 0xc0, 0xd4, 0xa0, +0xcd, 0x27, 0xaf, 0xbd, 0xcb, 0x86, 0x36, 0xc6, 0xcc, 0xfe, 0x59, 0xd2, +0xca, 0x90, 0x93, 0x36, 0x70, 0xaf, 0x9c, 0xe4, 0xcb, 0x6f, 0x65, 0x54, +0xd9, 0x47, 0x59, 0x70, 0xbb, 0x74, 0x1b, 0x0e, 0x89, 0xe7, 0xa3, 0xc7, +0x12, 0x39, 0x63, 0xea, 0x68, 0x12, 0x6b, 0x53, 0x5c, 0x9e, 0xef, 0x76, +0xf0, 0x55, 0x86, 0x0d, 0x17, 0x56, 0x9a, 0x4d, 0x94, 0x95, 0x65, 0xe6, +0xbe, 0x67, 0x98, 0xbe, 0xfb, 0x21, 0x52, 0xd2, 0x43, 0xaf, 0x5d, 0x47, +0x6b, 0x5c, 0xa3, 0x59, 0xbf, 0xc2, 0x62, 0xdd, 0x26, 0xa5, 0x12, 0x6a, +0x41, 0x44, 0xdf, 0xbd, 0xcd, 0x92, 0x17, 0xa0, 0xb6, 0x03, 0x43, 0xba, +0x66, 0x91, 0xe9, 0xdc, 0xc2, 0xce, 0xed, 0xa1, 0xfc, 0xc0, 0x2b, 0x14, +0xff, 0xfd, 0x1e, 0x4b, 0xb3, 0xa9, 0x29, 0x87, 0x81, 0xd2, 0x04, 0x8e, +0x66, 0x89, 0x58, 0x00, 0x7e, 0x07, 0xaf, 0xdb, 0xa4, 0xbb, 0xb5, 0x49, +0xb9, 0xaa, 0x18, 0xb9, 0x77, 0x8e, 0xcd, 0xdb, 0x6d, 0x1e, 0x1c, 0xb5, +0x38, 0x7d, 0xa5, 0xcf, 0xaa, 0x08, 0xeb, 0x77, 0x3f, 0x35, 0xc7, 0xda, +0xfc, 0x02, 0xaa, 0xf6, 0x1c, 0xbb, 0x9f, 0x78, 0x9f, 0x89, 0x43, 0x47, +0xa4, 0x6f, 0x3d, 0x06, 0xed, 0x90, 0x92, 0x79, 0x95, 0xd4, 0xe4, 0xfd, +0x98, 0x66, 0x4a, 0x6a, 0xd7, 0xc7, 0x0b, 0x62, 0xa4, 0xe3, 0x8c, 0x4d, +0xc4, 0xe8, 0x85, 0x98, 0xe5, 0x46, 0x44, 0x26, 0x97, 0x21, 0xe9, 0xf7, +0xf9, 0x61, 0xc5, 0xe3, 0xd4, 0x66, 0x84, 0xd2, 0x70, 0xc9, 0xee, 0x79, +0x98, 0x43, 0xc7, 0x5e, 0x27, 0xb6, 0x8a, 0xd2, 0x5a, 0x1f, 0xf3, 0xa9, +0xf7, 0x88, 0xce, 0x7d, 0x85, 0x71, 0xe0, 0x79, 0x98, 0x7a, 0x90, 0x9e, +0x1b, 0xd0, 0x13, 0x52, 0x4a, 0x66, 0x97, 0x7d, 0x33, 0x1e, 0xed, 0xae, +0xc7, 0x87, 0x1f, 0x7d, 0xce, 0xc2, 0xd5, 0x3a, 0xe6, 0xde, 0x02, 0xcb, +0xdb, 0x3e, 0xbe, 0xa6, 0x91, 0x95, 0x62, 0x6b, 0x2f, 0xce, 0x90, 0x3c, +0xfd, 0xce, 0x71, 0x0e, 0xcc, 0x3e, 0x82, 0x13, 0xf4, 0x09, 0xd5, 0x00, +0x16, 0x82, 0x98, 0xb3, 0x49, 0x24, 0xb1, 0x83, 0xc8, 0xc0, 0xd6, 0x3a, +0x54, 0x33, 0xab, 0x14, 0x8c, 0x16, 0x4e, 0x38, 0xcc, 0xe5, 0xeb, 0x4d, +0x5e, 0x7b, 0xfb, 0x4d, 0xaa, 0x79, 0xa1, 0x45, 0x1c, 0x9b, 0xd2, 0x94, +0xcc, 0x0e, 0x8c, 0x52, 0x7a, 0x65, 0x17, 0xc7, 0xa9, 0x0c, 0x8e, 0xe2, +0xf7, 0xba, 0xa8, 0xc8, 0x13, 0x87, 0x32, 0x87, 0x0b, 0x27, 0x30, 0x36, +0x57, 0xe8, 0xea, 0x15, 0xce, 0x06, 0x65, 0x5e, 0x3d, 0x5a, 0x94, 0x53, +0xb7, 0x59, 0x58, 0xdf, 0x25, 0xc4, 0xe4, 0xc9, 0x65, 0x3d, 0xb4, 0xb4, +0x4e, 0x37, 0x0c, 0x29, 0x98, 0x4a, 0xe8, 0x11, 0xde, 0x85, 0x42, 0x43, +0x1c, 0xaa, 0x38, 0x55, 0xc4, 0xb4, 0x2c, 0x22, 0x3d, 0xcd, 0xfa, 0xea, +0x0d, 0xf4, 0x8d, 0x1f, 0xc9, 0x5f, 0xfa, 0x82, 0x6d, 0xc7, 0xe1, 0xa6, +0x57, 0xe3, 0x56, 0x6e, 0x96, 0xbf, 0x16, 0x1f, 0xa3, 0x54, 0xaa, 0x91, +0x16, 0x5a, 0xb2, 0xa9, 0x04, 0xaf, 0x67, 0xc9, 0xac, 0x6c, 0xfa, 0x32, +0x9e, 0x48, 0xea, 0xa5, 0x0b, 0x89, 0x3b, 0x54, 0x47, 0xf2, 0xa1, 0xf2, +0x2a, 0x4d, 0xeb, 0xf4, 0x17, 0xdc, 0xd4, 0x72, 0x6c, 0xb5, 0x36, 0x28, +0xb6, 0x7e, 0x17, 0x04, 0xd3, 0xac, 0x7a, 0x42, 0xc1, 0xf4, 0x6e, 0x9e, +0xbf, 0x6b, 0xb7, 0x3c, 0x3a, 0x21, 0x67, 0xcb, 0x41, 0x48, 0x07, 0x91, +0xde, 0x1a, 0xe2, 0xaa, 0x9c, 0xb1, 0x59, 0xdb, 0x12, 0x25, 0xc1, 0x32, +0x92, 0xea, 0xc9, 0xaf, 0x3b, 0x97, 0xca, 0xca, 0xfe, 0x5b, 0xfe, 0xe5, +0x33, 0x29, 0xeb, 0x16, 0x95, 0xd2, 0x24, 0xeb, 0xda, 0x30, 0xeb, 0x95, +0x1a, 0xd3, 0xf7, 0x0f, 0x51, 0x1c, 0xd9, 0x0b, 0x99, 0x12, 0x7a, 0x4a, +0xd0, 0xd3, 0x25, 0x9a, 0x88, 0x45, 0xb1, 0x04, 0x33, 0x2c, 0x8a, 0x99, +0x34, 0x6b, 0x75, 0x19, 0x91, 0x9d, 0x92, 0x29, 0x89, 0xa0, 0x2c, 0x8b, +0x9d, 0xd8, 0x7a, 0x5e, 0x04, 0x07, 0x87, 0x66, 0x28, 0x56, 0x67, 0xb9, +0xd6, 0xd2, 0x39, 0xd9, 0xec, 0x33, 0x30, 0xb2, 0x8b, 0xea, 0xae, 0x83, +0x18, 0xb9, 0x31, 0x34, 0xbb, 0x42, 0x22, 0x0b, 0x21, 0x96, 0x3c, 0x61, +0xac, 0xcb, 0x95, 0x60, 0x2a, 0xe9, 0x68, 0x79, 0x08, 0x36, 0x56, 0x65, +0x27, 0x4a, 0xd9, 0x83, 0x00, 0xcf, 0x0b, 0xf1, 0xfc, 0x10, 0x15, 0x0a, +0x6a, 0x75, 0x77, 0x8b, 0x86, 0xdc, 0x58, 0x57, 0x45, 0x52, 0xe9, 0x84, +0x81, 0x7c, 0x91, 0x28, 0x55, 0x23, 0x96, 0x13, 0xd7, 0x24, 0xbe, 0xac, +0x17, 0xfa, 0xf2, 0x78, 0x63, 0xc7, 0x82, 0x08, 0xda, 0xa6, 0xc5, 0x50, +0x55, 0x04, 0xe5, 0x65, 0x5b, 0x06, 0xde, 0xce, 0xf0, 0x24, 0xf3, 0x4e, +0x70, 0xb5, 0x15, 0x6a, 0x34, 0x7b, 0x11, 0x9d, 0xbe, 0x10, 0x53, 0xd0, +0xa8, 0x86, 0x2e, 0x76, 0xb6, 0x2a, 0x9d, 0x2c, 0x48, 0x3c, 0x5b, 0xa2, +0xc8, 0x3a, 0x37, 0xd4, 0x9d, 0xed, 0x6c, 0x4a, 0xab, 0x95, 0x6e, 0x08, +0x66, 0x3d, 0x5a, 0xad, 0x4d, 0x18, 0xc8, 0xca, 0xfa, 0xd5, 0x85, 0x6f, +0xf9, 0x5f, 0xde, 0x02, 0x30, 0xff, 0x03, 0x8c, 0x47, 0x35, 0xad, 0xbc, +0xbf, 0x26, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, +0x42, 0x60, 0x82 + +}; + +unsigned int _Users_username_Pictures_SaltBae_png_len = 1863; + +int main(int argc, const char * argv[]) { + // insert code here... + int frameless = 0; + int resizable = 1; + int zoomable = 0; + int fullscreen = 1; + int fullSizeContent = 1; + int hideTitleBar = 0; + int titlebarAppearsTransparent = 0; + int hideTitle = 0; + int useToolbar = 0; + int hideToolbarSeparator = 0; + int webviewIsTransparent = 1; + int alwaysOnTop = 0; + int hideWindowOnClose = 0; + const char* appearance = "NSAppearanceNameDarkAqua"; + int windowIsTranslucent = 1; + int devtoolsEnabled = 1; + int defaultContextMenuEnabled = 1; + int windowStartState = 0; + int startsHidden = 0; + WailsContext *result = Create("OI OI!",400,400, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, + startsHidden, 400, 400, 600, 600, false); + SetBackgroundColour(result, 255, 0, 0, 255); + void *m = NewMenu(""); + SetAbout(result, "Fake title", "I am a description", _Users_username_Pictures_SaltBae_png, _Users_username_Pictures_SaltBae_png_len); +// AddMenuByRole(result, 1); + + AppendRole(result, m, 1); + AppendRole(result, m, 2); + void* submenu = NewMenu("test"); + void* menuITem = AppendMenuItem(result, submenu, "Woohoo", "p", 0, 0, 0, 470); + AppendSubmenu(m, submenu); + UpdateMenuItem(menuITem, 1); + SetAsApplicationMenu(result, m); +// SetPosition(result, 100, 100); + + + + Run((void*)CFBridgingRetain(result)); + return 0; +} diff --git a/v2/internal/frontend/desktop/darwin/menu.go b/v2/internal/frontend/desktop/darwin/menu.go new file mode 100644 index 000000000..24dbe3201 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/menu.go @@ -0,0 +1,134 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/menu/keys" +) + +type NSMenu struct { + context unsafe.Pointer + nsmenu unsafe.Pointer +} + +func NewNSMenu(context unsafe.Pointer, name string) *NSMenu { + c := NewCalloc() + defer c.Free() + title := c.String(name) + nsmenu := C.NewMenu(title) + return &NSMenu{ + context: context, + nsmenu: nsmenu, + } +} + +func (m *NSMenu) AddSubMenu(label string) *NSMenu { + result := NewNSMenu(m.context, label) + C.AppendSubmenu(m.nsmenu, result.nsmenu) + return result +} + +func (m *NSMenu) AppendRole(role menu.Role) { + C.AppendRole(m.context, m.nsmenu, C.int(role)) +} + +type MenuItem struct { + id uint + nsmenuitem unsafe.Pointer + wailsMenuItem *menu.MenuItem + radioGroupMembers []*MenuItem +} + +func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem { + c := NewCalloc() + defer c.Free() + var modifier C.int + var key *C.char + if menuItem.Accelerator != nil { + modifier = C.int(keys.ToMacModifier(menuItem.Accelerator)) + key = c.String(menuItem.Accelerator.Key) + } + + result := &MenuItem{ + wailsMenuItem: menuItem, + } + + result.id = createMenuItemID(result) + result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id)) + return result +} + +//func (w *Window) SetApplicationMenu(menu *menu.Menu) { +//w.applicationMenu = menu +//processMenu(w, menu) +//} + +func processMenu(parent *NSMenu, wailsMenu *menu.Menu) { + var radioGroups []*MenuItem + + for _, menuItem := range wailsMenu.Items { + if menuItem.SubMenu != nil { + if len(radioGroups) > 0 { + processRadioGroups(radioGroups) + radioGroups = []*MenuItem{} + } + submenu := parent.AddSubMenu(menuItem.Label) + processMenu(submenu, menuItem.SubMenu) + } else { + lastMenuItem := processMenuItem(parent, menuItem) + if menuItem.Type == menu.RadioType { + radioGroups = append(radioGroups, lastMenuItem) + } else { + if len(radioGroups) > 0 { + processRadioGroups(radioGroups) + radioGroups = []*MenuItem{} + } + } + } + } +} + +func processRadioGroups(groups []*MenuItem) { + for _, item := range groups { + item.radioGroupMembers = groups + } +} + +func processMenuItem(parent *NSMenu, menuItem *menu.MenuItem) *MenuItem { + if menuItem.Hidden { + return nil + } + if menuItem.Role != 0 { + parent.AppendRole(menuItem.Role) + return nil + } + if menuItem.Type == menu.SeparatorType { + C.AppendSeparator(parent.nsmenu) + return nil + } + + return parent.AddMenuItem(menuItem) +} + +func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { + f.mainWindow.SetApplicationMenu(menu) +} + +func (f *Frontend) MenuUpdateApplicationMenu() { + f.mainWindow.UpdateApplicationMenu() +} diff --git a/v2/internal/frontend/desktop/darwin/menuitem.go b/v2/internal/frontend/desktop/darwin/menuitem.go new file mode 100644 index 000000000..64aab84a9 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/menuitem.go @@ -0,0 +1,54 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + +import ( + "log" + "math" + "sync" +) + +var ( + menuItemToID = make(map[*MenuItem]uint) + idToMenuItem = make(map[uint]*MenuItem) + menuItemLock sync.Mutex + menuItemIDCounter uint = 0 +) + +func createMenuItemID(item *MenuItem) uint { + menuItemLock.Lock() + defer menuItemLock.Unlock() + counter := 0 + for { + menuItemIDCounter++ + value := idToMenuItem[menuItemIDCounter] + if value == nil { + break + } + counter++ + if counter == math.MaxInt { + log.Fatal("insane amounts of menuitems detected! Aborting before the collapse of the world!") + } + } + idToMenuItem[menuItemIDCounter] = item + menuItemToID[item] = menuItemIDCounter + return menuItemIDCounter +} + +func getMenuItemForID(id uint) *MenuItem { + menuItemLock.Lock() + defer menuItemLock.Unlock() + return idToMenuItem[id] +} diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h new file mode 100644 index 000000000..86506f868 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -0,0 +1,30 @@ +// +// message.h +// test +// +// Created by Lea Anthony on 14/10/21. +// + +#ifndef export_h +#define export_h + + +#ifdef __cplusplus +extern "C" +{ +#endif + +void processMessage(const char *); +void processBindingMessage(const char *, const char *, bool); +void processURLRequest(void *, void*); +void processMessageDialogResponse(int); +void processOpenFileDialogResponse(const char*); +void processSaveFileDialogResponse(const char*); +void processCallback(int); + +#ifdef __cplusplus +} +#endif + + +#endif /* export_h */ diff --git a/v2/internal/frontend/desktop/darwin/screen.go b/v2/internal/frontend/desktop/darwin/screen.go new file mode 100644 index 000000000..bd64a31f9 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/screen.go @@ -0,0 +1,118 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit -framework AppKit +#import +#include +#include + +#import "Application.h" +#import "WailsContext.h" + +typedef struct Screen { + int isCurrent; + int isPrimary; + int height; + int width; + int pHeight; + int pWidth; +} Screen; + + +int GetNumScreens(){ + return [[NSScreen screens] count]; +} + +int screenUniqueID(NSScreen *screen){ + // adapted from https://stackoverflow.com/a/1237490/4188138 + NSDictionary* screenDictionary = [screen deviceDescription]; + NSNumber* screenID = [screenDictionary objectForKey:@"NSScreenNumber"]; + CGDirectDisplayID aID = [screenID unsignedIntValue]; + return aID; +} + +Screen GetNthScreen(int nth, void *inctx){ + WailsContext *ctx = (__bridge WailsContext*) inctx; + NSArray *screens = [NSScreen screens]; + NSScreen* nthScreen = [screens objectAtIndex:nth]; + NSScreen* currentScreen = [ctx getCurrentScreen]; + + Screen returnScreen; + returnScreen.isCurrent = (int)(screenUniqueID(currentScreen)==screenUniqueID(nthScreen)); + // TODO properly handle screen mirroring + // from apple documentation: + // https://developer.apple.com/documentation/appkit/nsscreen/1388393-screens?language=objc + // The screen at index 0 in the returned array corresponds to the primary screen of the user’s system. This is the screen that contains the menu bar and whose origin is at the point (0, 0). In the case of mirroring, the first screen is the largest drawable display; if all screens are the same size, it is the screen with the highest pixel depth. This primary screen may not be the same as the one returned by the mainScreen method, which returns the screen with the active window. + returnScreen.isPrimary = nth==0; + returnScreen.height = (int) nthScreen.frame.size.height; + returnScreen.width = (int) nthScreen.frame.size.width; + + returnScreen.pWidth = 0; + returnScreen.pHeight = 0; + + // https://stackoverflow.com/questions/13859109/how-to-programmatically-determine-native-pixel-resolution-of-retina-macbook-pro + CGDirectDisplayID sid = ((NSNumber *)[nthScreen.deviceDescription + objectForKey:@"NSScreenNumber"]).unsignedIntegerValue; + + CFArrayRef ms = CGDisplayCopyAllDisplayModes(sid, NULL); + CFIndex n = CFArrayGetCount(ms); + for (int i = 0; i < n; i++) { + CGDisplayModeRef m = (CGDisplayModeRef) CFArrayGetValueAtIndex(ms, i); + if (CGDisplayModeGetIOFlags(m) & kDisplayModeNativeFlag) { + // This corresponds with "System Settings" -> General -> About -> Displays + returnScreen.pWidth = CGDisplayModeGetPixelWidth(m); + returnScreen.pHeight = CGDisplayModeGetPixelHeight(m); + break; + } + } + CFRelease(ms); + + if (returnScreen.pWidth == 0 || returnScreen.pHeight == 0) { + // If there was no native resolution take a best fit approach and use the backing pixel size. + NSRect pSize = [nthScreen convertRectToBacking:nthScreen.frame]; + returnScreen.pHeight = (int) pSize.size.height; + returnScreen.pWidth = (int) pSize.size.width; + } + return returnScreen; +} + +*/ +import "C" + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +func GetAllScreens(wailsContext unsafe.Pointer) ([]frontend.Screen, error) { + err := error(nil) + screens := []frontend.Screen{} + numScreens := int(C.GetNumScreens()) + for screeNum := 0; screeNum < numScreens; screeNum++ { + screenNumC := C.int(screeNum) + cScreen := C.GetNthScreen(screenNumC, wailsContext) + + screen := frontend.Screen{ + Height: int(cScreen.height), + Width: int(cScreen.width), + IsCurrent: cScreen.isCurrent == C.int(1), + IsPrimary: cScreen.isPrimary == C.int(1), + + Size: frontend.ScreenSize{ + Height: int(cScreen.height), + Width: int(cScreen.width), + }, + PhysicalSize: frontend.ScreenSize{ + Height: int(cScreen.pHeight), + Width: int(cScreen.pWidth), + }, + } + screens = append(screens, screen) + } + return screens, err +} diff --git a/v2/internal/frontend/desktop/darwin/single_instance.go b/v2/internal/frontend/desktop/darwin/single_instance.go new file mode 100644 index 000000000..27b34045b --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/single_instance.go @@ -0,0 +1,95 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa +#import "AppDelegate.h" + +*/ +import "C" + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +func SetupSingleInstance(uniqueID string) *os.File { + lockFilePath := getTempDir() + lockFileName := uniqueID + ".lock" + file, err := createLockFile(lockFilePath + "/" + lockFileName) + // if lockFile exist – send notification to second instance + if err != nil { + c := NewCalloc() + defer c.Free() + singleInstanceUniqueId := c.String(uniqueID) + + data, err := options.NewSecondInstanceData() + if err != nil { + return nil + } + + serialized, err := json.Marshal(data) + if err != nil { + return nil + } + + C.SendDataToFirstInstance(singleInstanceUniqueId, c.String(string(serialized))) + + os.Exit(0) + } + + return file +} + +//export HandleSecondInstanceData +func HandleSecondInstanceData(secondInstanceMessage *C.char) { + message := C.GoString(secondInstanceMessage) + + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(message), &secondInstanceData) + if err == nil { + secondInstanceBuffer <- secondInstanceData + } +} + +// createLockFile tries to create a file with given name and acquire an +// exclusive lock on it. If the file already exists AND is still locked, it will +// fail. +func createLockFile(filename string) (*os.File, error) { + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + fmt.Printf("Failed to open lockfile %s: %s", filename, err) + return nil, err + } + + err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + // Flock failed for some other reason than other instance already lock it. Print it in logs for possible debugging. + if !strings.Contains(err.Error(), "resource temporarily unavailable") { + fmt.Printf("Failed to lock lockfile %s: %s", filename, err) + } + file.Close() + return nil, err + } + + return file, nil +} + +// If app is sandboxed, golang os.TempDir() will return path that will not be accessible. So use native macOS temp dir function. +func getTempDir() string { + cstring := C.GetMacOsNativeTempDir() + path := C.GoString(cstring) + C.free(unsafe.Pointer(cstring)) + + return path +} diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go new file mode 100644 index 000000000..87d4213d9 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -0,0 +1,313 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + +import ( + "log" + "runtime" + "strconv" + "strings" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +func init() { + runtime.LockOSThread() +} + +type Window struct { + context unsafe.Pointer + + applicationMenu *menu.Menu +} + +func bool2Cint(value bool) C.int { + if value { + return C.int(1) + } + return C.int(0) +} + +func bool2CboolPtr(value bool) *C.bool { + v := C.bool(value) + return &v +} + +func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window { + c := NewCalloc() + defer c.Free() + + frameless := bool2Cint(frontendOptions.Frameless) + resizable := bool2Cint(!frontendOptions.DisableResize) + fullscreen := bool2Cint(frontendOptions.Fullscreen) + alwaysOnTop := bool2Cint(frontendOptions.AlwaysOnTop) + hideWindowOnClose := bool2Cint(frontendOptions.HideWindowOnClose) + startsHidden := bool2Cint(frontendOptions.StartHidden) + devtoolsEnabled := bool2Cint(devtools) + defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu) + singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil) + + var fullSizeContent, hideTitleBar, zoomable, hideTitle, useToolbar, webviewIsTransparent C.int + var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent, contentProtection C.int + var appearance, title *C.char + var preferences C.struct_Preferences + + width := C.int(frontendOptions.Width) + height := C.int(frontendOptions.Height) + minWidth := C.int(frontendOptions.MinWidth) + minHeight := C.int(frontendOptions.MinHeight) + maxWidth := C.int(frontendOptions.MaxWidth) + maxHeight := C.int(frontendOptions.MaxHeight) + windowStartState := C.int(int(frontendOptions.WindowStartState)) + + title = c.String(frontendOptions.Title) + + singleInstanceUniqueIdStr := "" + if frontendOptions.SingleInstanceLock != nil { + singleInstanceUniqueIdStr = frontendOptions.SingleInstanceLock.UniqueId + } + singleInstanceUniqueId := c.String(singleInstanceUniqueIdStr) + + enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection) + + enableDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.EnableFileDrop) + disableWebViewDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.DisableWebViewDrop) + + if frontendOptions.Mac != nil { + mac := frontendOptions.Mac + if mac.TitleBar != nil { + fullSizeContent = bool2Cint(mac.TitleBar.FullSizeContent) + hideTitleBar = bool2Cint(mac.TitleBar.HideTitleBar) + hideTitle = bool2Cint(mac.TitleBar.HideTitle) + useToolbar = bool2Cint(mac.TitleBar.UseToolbar) + titlebarAppearsTransparent = bool2Cint(mac.TitleBar.TitlebarAppearsTransparent) + hideToolbarSeparator = bool2Cint(mac.TitleBar.HideToolbarSeparator) + } + + if mac.Preferences != nil { + if mac.Preferences.TabFocusesLinks.IsSet() { + preferences.tabFocusesLinks = bool2CboolPtr(mac.Preferences.TabFocusesLinks.Get()) + } + + if mac.Preferences.TextInteractionEnabled.IsSet() { + preferences.textInteractionEnabled = bool2CboolPtr(mac.Preferences.TextInteractionEnabled.Get()) + } + + if mac.Preferences.FullscreenEnabled.IsSet() { + preferences.fullscreenEnabled = bool2CboolPtr(mac.Preferences.FullscreenEnabled.Get()) + } + } + + zoomable = bool2Cint(!frontendOptions.Mac.DisableZoom) + + windowIsTranslucent = bool2Cint(mac.WindowIsTranslucent) + webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent) + contentProtection = bool2Cint(mac.ContentProtection) + + appearance = c.String(string(mac.Appearance)) + } + var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, zoomable, fullscreen, fullSizeContent, + hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, + alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, contentProtection, devtoolsEnabled, defaultContextMenuEnabled, + windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, + preferences, singleInstanceEnabled, singleInstanceUniqueId, enableDragAndDrop, disableWebViewDragAndDrop, + ) + + // Create menu + result := &Window{ + context: unsafe.Pointer(context), + } + + if frontendOptions.BackgroundColour != nil { + result.SetBackgroundColour(frontendOptions.BackgroundColour.R, frontendOptions.BackgroundColour.G, frontendOptions.BackgroundColour.B, frontendOptions.BackgroundColour.A) + } + + if frontendOptions.Mac != nil && frontendOptions.Mac.About != nil { + title := c.String(frontendOptions.Mac.About.Title) + description := c.String(frontendOptions.Mac.About.Message) + var icon unsafe.Pointer + var length C.int + if frontendOptions.Mac.About.Icon != nil { + icon = unsafe.Pointer(&frontendOptions.Mac.About.Icon[0]) + length = C.int(len(frontendOptions.Mac.About.Icon)) + } + C.SetAbout(result.context, title, description, icon, length) + } + + if frontendOptions.Menu != nil { + result.SetApplicationMenu(frontendOptions.Menu) + } + + if debug && frontendOptions.Debug.OpenInspectorOnStartup { + showInspector(result.context) + } + return result +} + +func (w *Window) Center() { + C.Center(w.context) +} + +func (w *Window) Run(url string) { + _url := C.CString(url) + C.Run(w.context, _url) + C.free(unsafe.Pointer(_url)) +} + +func (w *Window) Quit() { + C.Quit(w.context) +} + +func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) { + C.SetBackgroundColour(w.context, C.int(r), C.int(g), C.int(b), C.int(a)) +} + +func (w *Window) ExecJS(js string) { + _js := C.CString(js) + C.ExecJS(w.context, _js) + C.free(unsafe.Pointer(_js)) +} + +func (w *Window) SetPosition(x int, y int) { + C.SetPosition(w.context, C.int(x), C.int(y)) +} + +func (w *Window) SetSize(width int, height int) { + C.SetSize(w.context, C.int(width), C.int(height)) +} + +func (w *Window) SetAlwaysOnTop(onTop bool) { + C.SetAlwaysOnTop(w.context, bool2Cint(onTop)) +} + +func (w *Window) SetTitle(title string) { + t := C.CString(title) + C.SetTitle(w.context, t) + C.free(unsafe.Pointer(t)) +} + +func (w *Window) Maximise() { + C.Maximise(w.context) +} + +func (w *Window) ToggleMaximise() { + C.ToggleMaximise(w.context) +} + +func (w *Window) UnMaximise() { + C.UnMaximise(w.context) +} + +func (w *Window) IsMaximised() bool { + return (bool)(C.IsMaximised(w.context)) +} + +func (w *Window) Minimise() { + C.Minimise(w.context) +} + +func (w *Window) UnMinimise() { + C.UnMinimise(w.context) +} + +func (w *Window) IsMinimised() bool { + return (bool)(C.IsMinimised(w.context)) +} + +func (w *Window) IsNormal() bool { + return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen() +} + +func (w *Window) SetMinSize(width int, height int) { + C.SetMinSize(w.context, C.int(width), C.int(height)) +} + +func (w *Window) SetMaxSize(width int, height int) { + C.SetMaxSize(w.context, C.int(width), C.int(height)) +} + +func (w *Window) Fullscreen() { + C.Fullscreen(w.context) +} + +func (w *Window) UnFullscreen() { + C.UnFullscreen(w.context) +} + +func (w *Window) IsFullScreen() bool { + return (bool)(C.IsFullScreen(w.context)) +} + +func (w *Window) Show() { + C.Show(w.context) +} + +func (w *Window) Hide() { + C.Hide(w.context) +} + +func (w *Window) ShowApplication() { + C.ShowApplication(w.context) +} + +func (w *Window) HideApplication() { + C.HideApplication(w.context) +} + +func parseIntDuo(temp string) (int, int) { + split := strings.Split(temp, ",") + x, err := strconv.Atoi(split[0]) + if err != nil { + log.Fatal(err) + } + y, err := strconv.Atoi(split[1]) + if err != nil { + log.Fatal(err) + } + return x, y +} + +func (w *Window) GetPosition() (int, int) { + var _result *C.char = C.GetPosition(w.context) + temp := C.GoString(_result) + return parseIntDuo(temp) +} + +func (w *Window) Size() (int, int) { + var _result *C.char = C.GetSize(w.context) + temp := C.GoString(_result) + return parseIntDuo(temp) +} + +func (w *Window) SetApplicationMenu(inMenu *menu.Menu) { + w.applicationMenu = inMenu + w.UpdateApplicationMenu() +} + +func (w *Window) UpdateApplicationMenu() { + mainMenu := NewNSMenu(w.context, "") + if w.applicationMenu != nil { + processMenu(mainMenu, w.applicationMenu) + } + C.SetAsApplicationMenu(w.context, mainMenu.nsmenu) + C.UpdateApplicationMenu(w.context) +} + +func (w Window) Print() { + C.WindowPrint(w.context) +} diff --git a/v2/internal/frontend/desktop/desktop_darwin.go b/v2/internal/frontend/desktop/desktop_darwin.go new file mode 100644 index 000000000..c02c0cdbd --- /dev/null +++ b/v2/internal/frontend/desktop/desktop_darwin.go @@ -0,0 +1,20 @@ +//go:build darwin +// +build darwin + +package desktop + +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend { + return darwin.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher) +} diff --git a/v2/internal/frontend/desktop/desktop_linux.go b/v2/internal/frontend/desktop/desktop_linux.go new file mode 100644 index 000000000..e057a9e18 --- /dev/null +++ b/v2/internal/frontend/desktop/desktop_linux.go @@ -0,0 +1,17 @@ +//go:build linux +// +build linux + +package desktop + +import ( + "context" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/linux" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend { + return linux.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher) +} diff --git a/v2/internal/frontend/desktop/desktop_windows.go b/v2/internal/frontend/desktop/desktop_windows.go new file mode 100644 index 000000000..f9a680aac --- /dev/null +++ b/v2/internal/frontend/desktop/desktop_windows.go @@ -0,0 +1,17 @@ +//go:build windows +// +build windows + +package desktop + +import ( + "context" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend { + return windows.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher) +} diff --git a/v2/internal/frontend/desktop/linux/browser.go b/v2/internal/frontend/desktop/linux/browser.go new file mode 100644 index 000000000..962e3b28a --- /dev/null +++ b/v2/internal/frontend/desktop/linux/browser.go @@ -0,0 +1,23 @@ +//go:build linux +// +build linux + +package linux + +import ( + "fmt" + "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" +) + +// BrowserOpenURL Use the default browser to open the url +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } + // Specific method implementation + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } +} diff --git a/v2/internal/frontend/desktop/linux/calloc.go b/v2/internal/frontend/desktop/linux/calloc.go new file mode 100644 index 000000000..27e5ad18d --- /dev/null +++ b/v2/internal/frontend/desktop/linux/calloc.go @@ -0,0 +1,35 @@ +//go:build linux +// +build linux + +package linux + +/* +#include +*/ +import "C" +import "unsafe" + +// Calloc handles alloc/dealloc of C data +type Calloc struct { + pool []unsafe.Pointer +} + +// NewCalloc creates a new allocator +func NewCalloc() Calloc { + return Calloc{} +} + +// String creates a new C string and retains a reference to it +func (c Calloc) String(in string) *C.char { + result := C.CString(in) + c.pool = append(c.pool, unsafe.Pointer(result)) + return result +} + +// Free frees all allocated C memory +func (c Calloc) Free() { + for _, str := range c.pool { + C.free(str) + } + c.pool = []unsafe.Pointer{} +} diff --git a/v2/internal/frontend/desktop/linux/clipboard.go b/v2/internal/frontend/desktop/linux/clipboard.go new file mode 100644 index 000000000..a2a46dacc --- /dev/null +++ b/v2/internal/frontend/desktop/linux/clipboard.go @@ -0,0 +1,51 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" + +static gchar* GetClipboardText() { + GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + return gtk_clipboard_wait_for_text(clip); +} + +static void SetClipboardText(gchar* text) { + GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text(clip, text, -1); + + clip = gtk_clipboard_get(GDK_SELECTION_PRIMARY); + gtk_clipboard_set_text(clip, text, -1); +} +*/ +import "C" +import "sync" + +func (f *Frontend) ClipboardGetText() (string, error) { + var text string + var wg sync.WaitGroup + wg.Add(1) + invokeOnMainThread(func() { + ctxt := C.GetClipboardText() + defer C.g_free(C.gpointer(ctxt)) + text = C.GoString(ctxt) + wg.Done() + }) + wg.Wait() + return text, nil +} + +func (f *Frontend) ClipboardSetText(text string) error { + invokeOnMainThread(func() { + ctxt := (*C.gchar)(C.CString(text)) + defer C.g_free(C.gpointer(ctxt)) + C.SetClipboardText(ctxt) + }) + return nil +} diff --git a/v2/internal/frontend/desktop/linux/dialog.go b/v2/internal/frontend/desktop/linux/dialog.go new file mode 100644 index 000000000..c0d9158e5 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/dialog.go @@ -0,0 +1,89 @@ +//go:build linux +// +build linux + +package linux + +import ( + "github.com/wailsapp/wails/v2/internal/frontend" + "unsafe" +) + +/* +#include +#include "gtk/gtk.h" +*/ +import "C" + +const ( + GTK_FILE_CHOOSER_ACTION_OPEN C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_OPEN + GTK_FILE_CHOOSER_ACTION_SAVE C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_SAVE + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER +) + +var openFileResults = make(chan []string) +var messageDialogResult = make(chan string) + +func (f *Frontend) OpenFileDialog(dialogOptions frontend.OpenDialogOptions) (result string, err error) { + f.mainWindow.OpenFileDialog(dialogOptions, 0, GTK_FILE_CHOOSER_ACTION_OPEN) + results := <-openFileResults + if len(results) == 1 { + return results[0], nil + } + return "", nil +} + +func (f *Frontend) OpenMultipleFilesDialog(dialogOptions frontend.OpenDialogOptions) ([]string, error) { + f.mainWindow.OpenFileDialog(dialogOptions, 1, GTK_FILE_CHOOSER_ACTION_OPEN) + result := <-openFileResults + return result, nil +} + +func (f *Frontend) OpenDirectoryDialog(dialogOptions frontend.OpenDialogOptions) (string, error) { + f.mainWindow.OpenFileDialog(dialogOptions, 0, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) + result := <-openFileResults + if len(result) == 1 { + return result[0], nil + } + return "", nil +} + +func (f *Frontend) SaveFileDialog(dialogOptions frontend.SaveDialogOptions) (string, error) { + options := frontend.OpenDialogOptions{ + DefaultDirectory: dialogOptions.DefaultDirectory, + DefaultFilename: dialogOptions.DefaultFilename, + Title: dialogOptions.Title, + Filters: dialogOptions.Filters, + ShowHiddenFiles: dialogOptions.ShowHiddenFiles, + CanCreateDirectories: dialogOptions.CanCreateDirectories, + } + f.mainWindow.OpenFileDialog(options, 0, GTK_FILE_CHOOSER_ACTION_SAVE) + results := <-openFileResults + if len(results) == 1 { + return results[0], nil + } + return "", nil +} + +func (f *Frontend) MessageDialog(dialogOptions frontend.MessageDialogOptions) (string, error) { + f.mainWindow.MessageDialog(dialogOptions) + return <-messageDialogResult, nil +} + +//export processOpenFileResult +func processOpenFileResult(carray **C.char) { + // Create a Go slice from the C array + var result []string + goArray := (*[1024]*C.char)(unsafe.Pointer(carray))[:1024:1024] + for _, s := range goArray { + if s == nil { + break + } + result = append(result, C.GoString(s)) + } + openFileResults <- result +} + +//export processMessageDialogResult +func processMessageDialogResult(result *C.char) { + messageDialogResult <- C.GoString(result) +} diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go new file mode 100644 index 000000000..2942a112e --- /dev/null +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -0,0 +1,589 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" + +// CREDIT: https://github.com/rainycape/magick +#include +#include +#include +#include + +static void fix_signal(int signum) +{ + struct sigaction st; + + if (sigaction(signum, NULL, &st) < 0) { + goto fix_signal_error; + } + st.sa_flags |= SA_ONSTACK; + if (sigaction(signum, &st, NULL) < 0) { + goto fix_signal_error; + } + return; +fix_signal_error: + fprintf(stderr, "error fixing handler for signal %d, please " + "report this issue to " + "https://github.com/wailsapp/wails: %s\n", + signum, strerror(errno)); +} + +static void install_signal_handlers() +{ +#if defined(SIGCHLD) + fix_signal(SIGCHLD); +#endif +#if defined(SIGHUP) + fix_signal(SIGHUP); +#endif +#if defined(SIGINT) + fix_signal(SIGINT); +#endif +#if defined(SIGQUIT) + fix_signal(SIGQUIT); +#endif +#if defined(SIGABRT) + fix_signal(SIGABRT); +#endif +#if defined(SIGFPE) + fix_signal(SIGFPE); +#endif +#if defined(SIGTERM) + fix_signal(SIGTERM); +#endif +#if defined(SIGBUS) + fix_signal(SIGBUS); +#endif +#if defined(SIGSEGV) + fix_signal(SIGSEGV); +#endif +#if defined(SIGXCPU) + fix_signal(SIGXCPU); +#endif +#if defined(SIGXFSZ) + fix_signal(SIGXFSZ); +#endif +} + +static gboolean install_signal_handlers_idle(gpointer data) { + (void)data; + install_signal_handlers(); + return G_SOURCE_REMOVE; +} + +static void fix_signal_handlers_after_gtk_init() { + g_idle_add(install_signal_handlers_idle, NULL); +} + +*/ +import "C" +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net" + "net/url" + "os" + "runtime" + "strings" + "sync" + "text/template" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/assetserver" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" + wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +var initOnce = sync.Once{} + +const startURL = "wails://wails/" + +var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) + +type Frontend struct { + + // Context + ctx context.Context + + frontendOptions *options.App + logger *logger.Logger + debug bool + devtoolsEnabled bool + + // Assets + assets *assetserver.AssetServer + startURL *url.URL + + // main window handle + mainWindow *Window + bindings *binding.Bindings + dispatcher frontend.Dispatcher + + originValidator *originvalidator.OriginValidator +} + +func (f *Frontend) RunMainLoop() { + C.gtk_main() +} + +func (f *Frontend) WindowClose() { + f.mainWindow.Destroy() +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + initOnce.Do(func() { + runtime.LockOSThread() + + // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings + if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") { + _ = os.Setenv("GDK_BACKEND", "x11") + } + + if ok := C.gtk_init_check(nil, nil); ok != 1 { + panic(errors.New("failed to init GTK")) + } + }) + + result := &Frontend{ + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + } + result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + + if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { + result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + } else { + if port, _ := ctx.Value("assetserverport").(string); port != "" { + result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + } + + var bindings string + var err error + if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated { + bindings, err = appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + } else { + appBindings.DB().UpdateObfuscatedCallMap() + } + assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, wailsruntime.RuntimeAssetsBundle) + if err != nil { + log.Fatal(err) + } + result.assets = assets + + go result.startRequestProcessor() + } + + go result.startMessageProcessor() + go result.startBindingsMessageProcessor() + + var _debug = ctx.Value("debug") + var _devtoolsEnabled = ctx.Value("devtoolsEnabled") + + if _debug != nil { + result.debug = _debug.(bool) + } + if _devtoolsEnabled != nil { + result.devtoolsEnabled = _devtoolsEnabled.(bool) + } + + result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled) + + C.fix_signal_handlers_after_gtk_init() + + if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" { + prgname := C.CString(appoptions.Linux.ProgramName) + C.g_set_prgname(prgname) + C.free(unsafe.Pointer(prgname)) + } + + go result.startSecondInstanceProcessor() + + return result +} + +func (f *Frontend) startMessageProcessor() { + for message := range messageBuffer { + f.processMessage(message) + } +} + +func (f *Frontend) startBindingsMessageProcessor() { + for msg := range bindingsMessageBuffer { + origin, err := f.originValidator.GetOriginFromURL(msg.source) + if err != nil { + f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err)) + continue + } + + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + continue + } + + f.processMessage(msg.message) + } +} + +func (f *Frontend) WindowReload() { + f.ExecJS("runtime.WindowReload();") +} + +func (f *Frontend) WindowSetSystemDefaultTheme() { + return +} + +func (f *Frontend) WindowSetLightTheme() { + return +} + +func (f *Frontend) WindowSetDarkTheme() { + return +} + +func (f *Frontend) Run(ctx context.Context) error { + f.ctx = ctx + + go func() { + if f.frontendOptions.OnStartup != nil { + f.frontendOptions.OnStartup(f.ctx) + } + }() + + if f.frontendOptions.SingleInstanceLock != nil { + SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + + f.mainWindow.Run(f.startURL.String()) + + return nil +} + +func (f *Frontend) WindowCenter() { + f.mainWindow.Center() +} + +func (f *Frontend) WindowSetAlwaysOnTop(b bool) { + f.mainWindow.SetKeepAbove(b) +} + +func (f *Frontend) WindowSetPosition(x, y int) { + f.mainWindow.SetPosition(x, y) +} +func (f *Frontend) WindowGetPosition() (int, int) { + return f.mainWindow.GetPosition() +} + +func (f *Frontend) WindowSetSize(width, height int) { + f.mainWindow.SetSize(width, height) +} + +func (f *Frontend) WindowGetSize() (int, int) { + return f.mainWindow.Size() +} + +func (f *Frontend) WindowSetTitle(title string) { + f.mainWindow.SetTitle(title) +} + +func (f *Frontend) WindowFullscreen() { + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = false;") + } + f.mainWindow.Fullscreen() +} + +func (f *Frontend) WindowUnfullscreen() { + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = true;") + } + f.mainWindow.UnFullscreen() +} + +func (f *Frontend) WindowReloadApp() { + f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL)) +} + +func (f *Frontend) WindowShow() { + f.mainWindow.Show() +} + +func (f *Frontend) WindowHide() { + f.mainWindow.Hide() +} + +func (f *Frontend) Show() { + f.mainWindow.Show() +} + +func (f *Frontend) Hide() { + f.mainWindow.Hide() +} +func (f *Frontend) WindowMaximise() { + f.mainWindow.Maximise() +} +func (f *Frontend) WindowToggleMaximise() { + f.mainWindow.ToggleMaximise() +} +func (f *Frontend) WindowUnmaximise() { + f.mainWindow.UnMaximise() +} +func (f *Frontend) WindowMinimise() { + f.mainWindow.Minimise() +} +func (f *Frontend) WindowUnminimise() { + f.mainWindow.UnMinimise() +} + +func (f *Frontend) WindowSetMinSize(width int, height int) { + f.mainWindow.SetMinSize(width, height) +} +func (f *Frontend) WindowSetMaxSize(width int, height int) { + f.mainWindow.SetMaxSize(width, height) +} + +func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) { + if col == nil { + return + } + f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A) +} + +func (f *Frontend) ScreenGetAll() ([]Screen, error) { + return GetAllScreens(f.mainWindow.asGTKWindow()) +} + +func (f *Frontend) WindowIsMaximised() bool { + return f.mainWindow.IsMaximised() +} + +func (f *Frontend) WindowIsMinimised() bool { + return f.mainWindow.IsMinimised() +} + +func (f *Frontend) WindowIsNormal() bool { + return f.mainWindow.IsNormal() +} + +func (f *Frontend) WindowIsFullscreen() bool { + return f.mainWindow.IsFullScreen() +} + +func (f *Frontend) Quit() { + if f.frontendOptions.OnBeforeClose != nil { + go func() { + if !f.frontendOptions.OnBeforeClose(f.ctx) { + f.mainWindow.Quit() + } + }() + return + } + f.mainWindow.Quit() +} + +func (f *Frontend) WindowPrint() { + f.ExecJS("window.print();") +} + +type EventNotify struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (f *Frontend) Notify(name string, data ...interface{}) { + notification := EventNotify{ + Name: name, + Data: data, + } + payload, err := json.Marshal(notification) + if err != nil { + f.logger.Error(err.Error()) + return + } + f.mainWindow.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`) +} + +var edgeMap = map[string]uintptr{ + "n-resize": C.GDK_WINDOW_EDGE_NORTH, + "ne-resize": C.GDK_WINDOW_EDGE_NORTH_EAST, + "e-resize": C.GDK_WINDOW_EDGE_EAST, + "se-resize": C.GDK_WINDOW_EDGE_SOUTH_EAST, + "s-resize": C.GDK_WINDOW_EDGE_SOUTH, + "sw-resize": C.GDK_WINDOW_EDGE_SOUTH_WEST, + "w-resize": C.GDK_WINDOW_EDGE_WEST, + "nw-resize": C.GDK_WINDOW_EDGE_NORTH_WEST, +} + +func (f *Frontend) processMessage(message string) { + if message == "DomReady" { + if f.frontendOptions.OnDomReady != nil { + f.frontendOptions.OnDomReady(f.ctx) + } + return + } + + if message == "drag" { + if !f.mainWindow.IsFullScreen() { + f.startDrag() + } + return + } + + if message == "wails:showInspector" { + f.mainWindow.ShowInspector() + return + } + + if strings.HasPrefix(message, "resize:") { + if !f.mainWindow.IsFullScreen() { + sl := strings.Split(message, ":") + if len(sl) != 2 { + f.logger.Info("Unknown message returned from dispatcher: %+v", message) + return + } + edge := edgeMap[sl[1]] + err := f.startResize(edge) + if err != nil { + f.logger.Error(err.Error()) + } + } + return + } + + if message == "runtime:ready" { + cmd := fmt.Sprintf( + "window.wails.setCSSDragProperties('%s', '%s');\n"+ + "window.wails.setCSSDropProperties('%s', '%s');\n"+ + "window.wails.flags.deferDragToMouseMove = true;", + f.frontendOptions.CSSDragProperty, + f.frontendOptions.CSSDragValue, + f.frontendOptions.DragAndDrop.CSSDropProperty, + f.frontendOptions.DragAndDrop.CSSDropValue, + ) + + f.ExecJS(cmd) + + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = true;") + } + + if f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + + return + } + + go func() { + result, err := f.dispatcher.ProcessMessage(message, f) + if err != nil { + f.logger.Error(err.Error()) + f.Callback(result) + return + } + if result == "" { + return + } + + switch result[0] { + case 'c': + // Callback from a method call + f.Callback(result[1:]) + default: + f.logger.Info("Unknown message returned from dispatcher: %+v", result) + } + }() +} + +func (f *Frontend) Callback(message string) { + escaped, err := json.Marshal(message) + if err != nil { + panic(err) + } + f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`) +} + +func (f *Frontend) startDrag() { + f.mainWindow.StartDrag() +} + +func (f *Frontend) startResize(edge uintptr) error { + f.mainWindow.StartResize(edge) + return nil +} + +func (f *Frontend) ExecJS(js string) { + f.mainWindow.ExecJS(js) +} + +type bindingsMessage struct { + message string + source string +} + +var messageBuffer = make(chan string, 100) +var bindingsMessageBuffer = make(chan *bindingsMessage, 100) + +//export processMessage +func processMessage(message *C.char) { + goMessage := C.GoString(message) + messageBuffer <- goMessage +} + +//export processBindingMessage +func processBindingMessage(message *C.char, source *C.char) { + goMessage := C.GoString(message) + goSource := C.GoString(source) + bindingsMessageBuffer <- &bindingsMessage{ + message: goMessage, + source: goSource, + } +} + +var requestBuffer = make(chan webview.Request, 100) + +func (f *Frontend) startRequestProcessor() { + for request := range requestBuffer { + f.assets.ServeWebViewRequest(request) + } +} + +//export processURLRequest +func processURLRequest(request unsafe.Pointer) { + requestBuffer <- webview.NewRequest(request) +} + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/linux/gtk.go b/v2/internal/frontend/desktop/linux/gtk.go new file mode 100644 index 000000000..67a38c7a0 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/gtk.go @@ -0,0 +1,85 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" + +static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); } + +extern void blockClick(GtkWidget* menuItem, gulong handler_id); +extern void unblockClick(GtkWidget* menuItem, gulong handler_id); +*/ +import "C" +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +func GtkMenuItemWithLabel(label string) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_menu_item_new_with_label(cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +func GtkCheckMenuItemWithLabel(label string) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_check_menu_item_new_with_label(cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +func GtkRadioMenuItemWithLabel(label string, group *C.GSList) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_radio_menu_item_new_with_label(group, cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +//export handleMenuItemClick +func handleMenuItemClick(gtkWidget unsafe.Pointer) { + // Make sure to execute the final callback on a new goroutine otherwise if the callback e.g. tries to open a dialog, the + // main thread will get blocked and so the message loop blocks. As a result the app will block and shows a + // "not responding" dialog. + + item := gtkSignalToMenuItem[(*C.GtkWidget)(gtkWidget)] + switch item.Type { + case menu.CheckboxType: + item.Checked = !item.Checked + checked := C.int(0) + if item.Checked { + checked = C.int(1) + } + for _, gtkCheckbox := range gtkCheckboxCache[item] { + handler := gtkSignalHandlers[gtkCheckbox] + C.blockClick(gtkCheckbox, handler) + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkCheckbox)), checked) + C.unblockClick(gtkCheckbox, handler) + } + go item.Click(&menu.CallbackData{MenuItem: item}) + case menu.RadioType: + gtkRadioItems := gtkRadioMenuCache[item] + active := C.gtk_check_menu_item_get_active(C.toGtkCheckMenuItem(gtkWidget)) + if int(active) == 1 { + for _, gtkRadioItem := range gtkRadioItems { + handler := gtkSignalHandlers[gtkRadioItem] + C.blockClick(gtkRadioItem, handler) + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkRadioItem)), 1) + C.unblockClick(gtkRadioItem, handler) + } + item.Checked = true + go item.Click(&menu.CallbackData{MenuItem: item}) + } else { + item.Checked = false + } + default: + go item.Click(&menu.CallbackData{MenuItem: item}) + } +} diff --git a/v2/internal/frontend/desktop/linux/invoke.go b/v2/internal/frontend/desktop/linux/invoke.go new file mode 100644 index 000000000..16d5e73d2 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/invoke.go @@ -0,0 +1,78 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 + +#include +#include "gtk/gtk.h" + +extern gboolean invokeCallbacks(void *); + +static inline void triggerInvokesOnMainThread() { + g_idle_add((GSourceFunc)invokeCallbacks, NULL); +} +*/ +import "C" +import ( + "runtime" + "sync" + "unsafe" + + "golang.org/x/sys/unix" +) + +var ( + m sync.Mutex + mainTid int + dispatchq []func() +) + +func invokeOnMainThread(f func()) { + if tryInvokeOnCurrentGoRoutine(f) { + return + } + + m.Lock() + dispatchq = append(dispatchq, f) + m.Unlock() + + C.triggerInvokesOnMainThread() +} + +func tryInvokeOnCurrentGoRoutine(f func()) bool { + m.Lock() + mainThreadID := mainTid + m.Unlock() + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if mainThreadID != unix.Gettid() { + return false + } + f() + return true +} + +//export invokeCallbacks +func invokeCallbacks(_ unsafe.Pointer) C.gboolean { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + m.Lock() + if mainTid == 0 { + mainTid = unix.Gettid() + } + + q := append([]func(){}, dispatchq...) + dispatchq = []func(){} + m.Unlock() + + for _, v := range q { + v() + } + return C.G_SOURCE_REMOVE +} diff --git a/v2/internal/frontend/desktop/linux/keys.go b/v2/internal/frontend/desktop/linux/keys.go new file mode 100644 index 000000000..e5a127dbd --- /dev/null +++ b/v2/internal/frontend/desktop/linux/keys.go @@ -0,0 +1,110 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + + +#include "gtk/gtk.h" + +*/ +import "C" +import ( + "github.com/wailsapp/wails/v2/pkg/menu/keys" +) + +var namedKeysToGTK = map[string]C.guint{ + "backspace": C.guint(0xff08), + "tab": C.guint(0xff09), + "return": C.guint(0xff0d), + "enter": C.guint(0xff0d), + "escape": C.guint(0xff1b), + "left": C.guint(0xff51), + "right": C.guint(0xff53), + "up": C.guint(0xff52), + "down": C.guint(0xff54), + "space": C.guint(0xff80), + "delete": C.guint(0xff9f), + "home": C.guint(0xff95), + "end": C.guint(0xff9c), + "page up": C.guint(0xff9a), + "page down": C.guint(0xff9b), + "f1": C.guint(0xffbe), + "f2": C.guint(0xffbf), + "f3": C.guint(0xffc0), + "f4": C.guint(0xffc1), + "f5": C.guint(0xffc2), + "f6": C.guint(0xffc3), + "f7": C.guint(0xffc4), + "f8": C.guint(0xffc5), + "f9": C.guint(0xffc6), + "f10": C.guint(0xffc7), + "f11": C.guint(0xffc8), + "f12": C.guint(0xffc9), + "f13": C.guint(0xffca), + "f14": C.guint(0xffcb), + "f15": C.guint(0xffcc), + "f16": C.guint(0xffcd), + "f17": C.guint(0xffce), + "f18": C.guint(0xffcf), + "f19": C.guint(0xffd0), + "f20": C.guint(0xffd1), + "f21": C.guint(0xffd2), + "f22": C.guint(0xffd3), + "f23": C.guint(0xffd4), + "f24": C.guint(0xffd5), + "f25": C.guint(0xffd6), + "f26": C.guint(0xffd7), + "f27": C.guint(0xffd8), + "f28": C.guint(0xffd9), + "f29": C.guint(0xffda), + "f30": C.guint(0xffdb), + "f31": C.guint(0xffdc), + "f32": C.guint(0xffdd), + "f33": C.guint(0xffde), + "f34": C.guint(0xffdf), + "f35": C.guint(0xffe0), + "numlock": C.guint(0xff7f), +} + +func acceleratorToGTK(accelerator *keys.Accelerator) (C.guint, C.GdkModifierType) { + key := parseKey(accelerator.Key) + mods := parseModifiers(accelerator.Modifiers) + return key, mods +} + +func parseKey(key string) C.guint { + var result C.guint + result, found := namedKeysToGTK[key] + if found { + return result + } + // Check for unknown namedkeys + // Check if we only have a single character + if len(key) != 1 { + return C.guint(0) + } + keyval := rune(key[0]) + return C.gdk_unicode_to_keyval(C.guint(keyval)) +} + +func parseModifiers(modifiers []keys.Modifier) C.GdkModifierType { + + var result C.GdkModifierType + + for _, modifier := range modifiers { + switch modifier { + case keys.ShiftKey: + result |= C.GDK_SHIFT_MASK + case keys.ControlKey, keys.CmdOrCtrlKey: + result |= C.GDK_CONTROL_MASK + case keys.OptionOrAltKey: + result |= C.GDK_MOD1_MASK + } + } + return result +} diff --git a/v2/internal/frontend/desktop/linux/menu.go b/v2/internal/frontend/desktop/linux/menu.go new file mode 100644 index 000000000..a61d190bd --- /dev/null +++ b/v2/internal/frontend/desktop/linux/menu.go @@ -0,0 +1,169 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" + +static GtkMenuItem *toGtkMenuItem(void *pointer) { return (GTK_MENU_ITEM(pointer)); } +static GtkMenuShell *toGtkMenuShell(void *pointer) { return (GTK_MENU_SHELL(pointer)); } +static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); } +static GtkRadioMenuItem *toGtkRadioMenuItem(void *pointer) { return (GTK_RADIO_MENU_ITEM(pointer)); } + +extern void handleMenuItemClick(void*); + +void blockClick(GtkWidget* menuItem, gulong handler_id) { + g_signal_handler_block (menuItem, handler_id); +} + +void unblockClick(GtkWidget* menuItem, gulong handler_id) { + g_signal_handler_unblock (menuItem, handler_id); +} + +gulong connectClick(GtkWidget* menuItem) { + return g_signal_connect(menuItem, "activate", G_CALLBACK(handleMenuItemClick), (void*)menuItem); +} + +void addAccelerator(GtkWidget* menuItem, GtkAccelGroup* group, guint key, GdkModifierType mods) { + gtk_widget_add_accelerator(menuItem, "activate", group, key, mods, GTK_ACCEL_VISIBLE); +} +*/ +import "C" +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +var menuIdCounter int +var menuItemToId map[*menu.MenuItem]int +var menuIdToItem map[int]*menu.MenuItem +var gtkCheckboxCache map[*menu.MenuItem][]*C.GtkWidget +var gtkMenuCache map[*menu.MenuItem]*C.GtkWidget +var gtkRadioMenuCache map[*menu.MenuItem][]*C.GtkWidget +var gtkSignalHandlers map[*C.GtkWidget]C.gulong +var gtkSignalToMenuItem map[*C.GtkWidget]*menu.MenuItem + +func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { + f.mainWindow.SetApplicationMenu(menu) +} + +func (f *Frontend) MenuUpdateApplicationMenu() { + f.mainWindow.SetApplicationMenu(f.mainWindow.applicationMenu) +} + +func (w *Window) SetApplicationMenu(inmenu *menu.Menu) { + if inmenu == nil { + return + } + + // Setup accelerator group + w.accels = C.gtk_accel_group_new() + C.gtk_window_add_accel_group(w.asGTKWindow(), w.accels) + + menuItemToId = make(map[*menu.MenuItem]int) + menuIdToItem = make(map[int]*menu.MenuItem) + gtkCheckboxCache = make(map[*menu.MenuItem][]*C.GtkWidget) + gtkMenuCache = make(map[*menu.MenuItem]*C.GtkWidget) + gtkRadioMenuCache = make(map[*menu.MenuItem][]*C.GtkWidget) + gtkSignalHandlers = make(map[*C.GtkWidget]C.gulong) + gtkSignalToMenuItem = make(map[*C.GtkWidget]*menu.MenuItem) + + // Increase ref count? + w.menubar = C.gtk_menu_bar_new() + + processMenu(w, inmenu) + + C.gtk_widget_show(w.menubar) +} + +func processMenu(window *Window, menu *menu.Menu) { + for _, menuItem := range menu.Items { + if menuItem.SubMenu != nil { + submenu := processSubmenu(menuItem, window.accels) + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu) + } + } +} + +func processSubmenu(menuItem *menu.MenuItem, group *C.GtkAccelGroup) *C.GtkWidget { + existingMenu := gtkMenuCache[menuItem] + if existingMenu != nil { + return existingMenu + } + gtkMenu := C.gtk_menu_new() + submenu := GtkMenuItemWithLabel(menuItem.Label) + for _, menuItem := range menuItem.SubMenu.Items { + menuID := menuIdCounter + menuIdToItem[menuID] = menuItem + menuItemToId[menuItem] = menuID + menuIdCounter++ + processMenuItem(gtkMenu, menuItem, group) + } + C.gtk_menu_item_set_submenu(C.toGtkMenuItem(unsafe.Pointer(submenu)), gtkMenu) + gtkMenuCache[menuItem] = existingMenu + return submenu +} + +var currentRadioGroup *C.GSList + +func processMenuItem(parent *C.GtkWidget, menuItem *menu.MenuItem, group *C.GtkAccelGroup) { + if menuItem.Hidden { + return + } + + if menuItem.Type != menu.RadioType { + currentRadioGroup = nil + } + + if menuItem.Type == menu.SeparatorType { + result := C.gtk_separator_menu_item_new() + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result) + return + } + + var result *C.GtkWidget + + switch menuItem.Type { + case menu.TextType: + result = GtkMenuItemWithLabel(menuItem.Label) + case menu.CheckboxType: + result = GtkCheckMenuItemWithLabel(menuItem.Label) + if menuItem.Checked { + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1) + } + gtkCheckboxCache[menuItem] = append(gtkCheckboxCache[menuItem], result) + + case menu.RadioType: + result = GtkRadioMenuItemWithLabel(menuItem.Label, currentRadioGroup) + currentRadioGroup = C.gtk_radio_menu_item_get_group(C.toGtkRadioMenuItem(unsafe.Pointer(result))) + if menuItem.Checked { + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1) + } + gtkRadioMenuCache[menuItem] = append(gtkRadioMenuCache[menuItem], result) + case menu.SubmenuType: + result = processSubmenu(menuItem, group) + } + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result) + C.gtk_widget_show(result) + + if menuItem.Click != nil { + handler := C.connectClick(result) + gtkSignalHandlers[result] = handler + gtkSignalToMenuItem[result] = menuItem + } + + if menuItem.Disabled { + C.gtk_widget_set_sensitive(result, 0) + } + + if menuItem.Accelerator != nil { + key, mods := acceleratorToGTK(menuItem.Accelerator) + C.addAccelerator(result, group, key, mods) + } +} diff --git a/v2/internal/frontend/desktop/linux/screen.go b/v2/internal/frontend/desktop/linux/screen.go new file mode 100644 index 000000000..0a0507425 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/screen.go @@ -0,0 +1,91 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#cgo CFLAGS: -w +#include +#include "webkit2/webkit2.h" +#include "gtk/gtk.h" +#include "gdk/gdk.h" + +typedef struct Screen { + int isCurrent; + int isPrimary; + int height; + int width; + int scale; +} Screen; + +int GetNMonitors(GtkWindow *window){ + GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + GdkDisplay *display = gdk_window_get_display(gdk_window); + return gdk_display_get_n_monitors(display); +} + +Screen GetNThMonitor(int monitor_num, GtkWindow *window){ + GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + GdkDisplay *display = gdk_window_get_display(gdk_window); + GdkMonitor *monitor = gdk_display_get_monitor(display,monitor_num); + GdkMonitor *currentMonitor = gdk_display_get_monitor_at_window(display,gdk_window); + Screen screen; + GdkRectangle geometry; + gdk_monitor_get_geometry(monitor,&geometry); + screen.isCurrent = currentMonitor==monitor; + screen.isPrimary = gdk_monitor_is_primary(monitor); + screen.height = geometry.height; + screen.width = geometry.width; + screen.scale = gdk_monitor_get_scale_factor(monitor); + return screen; +} +*/ +import "C" +import ( + "sync" + + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type Screen = frontend.Screen + +func GetAllScreens(window *C.GtkWindow) ([]Screen, error) { + if window == nil { + return nil, errors.New("window is nil, cannot perform screen operations") + } + var wg sync.WaitGroup + var screens []Screen + wg.Add(1) + invokeOnMainThread(func() { + numMonitors := C.GetNMonitors(window) + for i := 0; i < int(numMonitors); i++ { + cMonitor := C.GetNThMonitor(C.int(i), window) + + screen := Screen{ + IsCurrent: cMonitor.isCurrent == 1, + IsPrimary: cMonitor.isPrimary == 1, + Width: int(cMonitor.width), + Height: int(cMonitor.height), + + Size: frontend.ScreenSize{ + Width: int(cMonitor.width), + Height: int(cMonitor.height), + }, + PhysicalSize: frontend.ScreenSize{ + Width: int(cMonitor.width * cMonitor.scale), + Height: int(cMonitor.height * cMonitor.scale), + }, + } + screens = append(screens, screen) + } + + wg.Done() + }) + wg.Wait() + return screens, nil +} diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go new file mode 100644 index 000000000..0317dee49 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -0,0 +1,77 @@ +//go:build linux +// +build linux + +package linux + +import ( + "encoding/json" + "github.com/godbus/dbus/v5" + "github.com/wailsapp/wails/v2/pkg/options" + "log" + "os" + "strings" +) + +type dbusHandler func(string) + +func (f dbusHandler) SendMessage(message string) *dbus.Error { + f(message) + return nil +} + +func SetupSingleInstance(uniqueID string) { + id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_") + + dbusName := "org." + id + ".SingleInstance" + dbusPath := "/org/" + id + "/SingleInstance" + + conn, err := dbus.ConnectSessionBus() + // if we will reach any error during establishing connection or sending message we will just continue. + // It should not be the case that such thing will happen actually, but just in case. + if err != nil { + return + } + + f := dbusHandler(func(message string) { + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(message), &secondInstanceData) + if err == nil { + secondInstanceBuffer <- secondInstanceData + } + }) + + err = conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + if err != nil { + return + } + + reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue) + if err != nil { + return + } + + // if name already taken, try to send args to existing instance, if no success just launch new instance + if reply == dbus.RequestNameReplyExists { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + data.WorkingDirectory, err = os.Getwd() + if err != nil { + log.Printf("Failed to get working directory: %v", err) + return + } + + serialized, err := json.Marshal(data) + if err != nil { + log.Printf("Failed to marshal data: %v", err) + return + } + + err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store() + if err != nil { + return + } + os.Exit(1) + } +} diff --git a/v2/internal/frontend/desktop/linux/webkit2.go b/v2/internal/frontend/desktop/linux/webkit2.go new file mode 100644 index 000000000..06e0c7824 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/webkit2.go @@ -0,0 +1,32 @@ +//go:build linux + +package linux + +/* +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 +#include "webkit2/webkit2.h" +*/ +import "C" +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/linux" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +func validateWebKit2Version(options *options.App) { + if C.webkit_get_major_version() == 2 && C.webkit_get_minor_version() >= webview.Webkit2MinMinorVersion { + return + } + + msg := linux.DefaultMessages() + if options.Linux != nil && options.Linux.Messages != nil { + msg = options.Linux.Messages + } + + v := fmt.Sprintf("2.%d.0", webview.Webkit2MinMinorVersion) + showModalDialogAndExit("WebKit2GTK", fmt.Sprintf(msg.WebKit2GTKMinRequired, v)) +} diff --git a/v2/internal/frontend/desktop/linux/window.c b/v2/internal/frontend/desktop/linux/window.c new file mode 100644 index 000000000..5441db022 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/window.c @@ -0,0 +1,891 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "window.h" + +// These are the x,y,time & button of the last mouse down event +// It's used for window dragging +static float xroot = 0.0f; +static float yroot = 0.0f; +static int dragTime = -1; +static uint mouseButton = 0; +static int wmIsWayland = -1; +static int decoratorWidth = -1; +static int decoratorHeight = -1; + +// casts +void ExecuteOnMainThread(void *f, gpointer jscallback) +{ + g_idle_add((GSourceFunc)f, (gpointer)jscallback); +} + +GtkWidget *GTKWIDGET(void *pointer) +{ + return GTK_WIDGET(pointer); +} + +GtkWindow *GTKWINDOW(void *pointer) +{ + return GTK_WINDOW(pointer); +} + +GtkContainer *GTKCONTAINER(void *pointer) +{ + return GTK_CONTAINER(pointer); +} + +GtkBox *GTKBOX(void *pointer) +{ + return GTK_BOX(pointer); +} + +extern void processMessage(char *); +extern void processBindingMessage(char *, char *); + +static void sendMessageToBackend(WebKitUserContentManager *contentManager, + WebKitJavascriptResult *result, + void *data) +{ + // Retrieve webview from content manager + WebKitWebView *webview = WEBKIT_WEB_VIEW(g_object_get_data(G_OBJECT(contentManager), "webview")); + const char *current_uri = webview ? webkit_web_view_get_uri(webview) : NULL; + char *uri = current_uri ? g_strdup(current_uri) : NULL; + +#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 + JSCValue *value = webkit_javascript_result_get_js_value(result); + char *message = jsc_value_to_string(value); +#else + JSGlobalContextRef context = webkit_javascript_result_get_global_context(result); + JSValueRef value = webkit_javascript_result_get_value(result); + JSStringRef js = JSValueToStringCopy(context, value, NULL); + size_t messageSize = JSStringGetMaximumUTF8CStringSize(js); + char *message = g_new(char, messageSize); + JSStringGetUTF8CString(js, message, messageSize); + JSStringRelease(js); +#endif + processBindingMessage(message, uri); + g_free(message); + if (uri) { + g_free(uri); + } +} + +static bool isNULLRectangle(GdkRectangle input) +{ + return input.x == -1 && input.y == -1 && input.width == -1 && input.height == -1; +} + +static gboolean onWayland() +{ + switch (wmIsWayland) + { + case -1: + { + char *gdkBackend = getenv("XDG_SESSION_TYPE"); + if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0) + { + wmIsWayland = 1; + return TRUE; + } + + wmIsWayland = 0; + return FALSE; + } + case 1: + return TRUE; + default: + return FALSE; + } +} + +static GdkMonitor *getCurrentMonitor(GtkWindow *window) +{ + // Get the monitor that the window is currently on + GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window)); + GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + if (gdk_window == NULL) + { + return NULL; + } + GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, gdk_window); + + return GDK_MONITOR(monitor); +} + +static GdkRectangle getCurrentMonitorGeometry(GtkWindow *window) +{ + GdkMonitor *monitor = getCurrentMonitor(window); + GdkRectangle result; + if (monitor == NULL) + { + result.x = result.y = result.height = result.width = -1; + return result; + } + + // Get the geometry of the monitor + gdk_monitor_get_geometry(monitor, &result); + return result; +} + +static int getCurrentMonitorScaleFactor(GtkWindow *window) +{ + GdkMonitor *monitor = getCurrentMonitor(window); + + return gdk_monitor_get_scale_factor(monitor); +} + +// window + +ulong SetupInvokeSignal(void *contentManager) +{ + return g_signal_connect((WebKitUserContentManager *)contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), NULL); +} + +void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len) +{ + GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); + if (!loader) + { + return; + } + if (gdk_pixbuf_loader_write(loader, buf, len, NULL) && gdk_pixbuf_loader_close(loader, NULL)) + { + GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader); + if (pixbuf) + { + gtk_window_set_icon(window, pixbuf); + } + } + g_object_unref(loader); +} + +void SetWindowTransparency(GtkWidget *widget) +{ + GdkScreen *screen = gtk_widget_get_screen(widget); + GdkVisual *visual = gdk_screen_get_rgba_visual(screen); + + if (visual != NULL && gdk_screen_is_composited(screen)) + { + gtk_widget_set_app_paintable(widget, true); + gtk_widget_set_visual(widget, visual); + } +} + +static GtkCssProvider *windowCssProvider = NULL; + +void SetBackgroundColour(void *data) +{ + // set webview's background color + RGBAOptions *options = (RGBAOptions *)data; + + GdkRGBA colour = {options->r / 255.0, options->g / 255.0, options->b / 255.0, options->a / 255.0}; + if (options->windowIsTranslucent != NULL && options->windowIsTranslucent == TRUE) + { + colour.alpha = 0.0; + } + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(options->webview), &colour); + + // set window's background color + // Get the name of the current locale + char *old_locale, *saved_locale; + old_locale = setlocale(LC_ALL, NULL); + + // Copy the name so it won’t be clobbered by setlocale. + saved_locale = strdup(old_locale); + if (saved_locale == NULL) + return; + + //Now change the locale to english for so printf always converts floats with a dot decimal separator + setlocale(LC_ALL, "en_US.UTF-8"); + gchar *str = g_strdup_printf("#webview-box {background-color: rgba(%d, %d, %d, %1.1f);}", options->r, options->g, options->b, options->a / 255.0); + + //Restore the original locale. + setlocale(LC_ALL, saved_locale); + free(saved_locale); + + if (windowCssProvider == NULL) + { + windowCssProvider = gtk_css_provider_new(); + gtk_style_context_add_provider( + gtk_widget_get_style_context(GTK_WIDGET(options->webviewBox)), + GTK_STYLE_PROVIDER(windowCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + g_object_unref(windowCssProvider); + } + + gtk_css_provider_load_from_data(windowCssProvider, str, -1, NULL); + g_free(str); +} + +static gboolean setTitle(gpointer data) +{ + SetTitleArgs *args = (SetTitleArgs *)data; + gtk_window_set_title(args->window, args->title); + free((void *)args->title); + free((void *)data); + + return G_SOURCE_REMOVE; +} + +void SetTitle(GtkWindow *window, char *title) +{ + SetTitleArgs *args = malloc(sizeof(SetTitleArgs)); + args->window = window; + args->title = title; + ExecuteOnMainThread(setTitle, (gpointer)args); +} + +static gboolean setPosition(gpointer data) +{ + SetPositionArgs *args = (SetPositionArgs *)data; + gtk_window_move((GtkWindow *)args->window, args->x, args->y); + free(args); + + return G_SOURCE_REMOVE; +} + +void SetPosition(void *window, int x, int y) +{ + GdkRectangle monitorDimensions = getCurrentMonitorGeometry(window); + if (isNULLRectangle(monitorDimensions)) + { + return; + } + SetPositionArgs *args = malloc(sizeof(SetPositionArgs)); + args->window = window; + args->x = monitorDimensions.x + x; + args->y = monitorDimensions.y + y; + ExecuteOnMainThread(setPosition, (gpointer)args); +} + +void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height) +{ + GdkGeometry size; + size.min_width = size.min_height = size.max_width = size.max_height = 0; + + GdkRectangle monitorSize = getCurrentMonitorGeometry(window); + if (isNULLRectangle(monitorSize)) + { + return; + } + + int flags = GDK_HINT_MAX_SIZE | GDK_HINT_MIN_SIZE; + + size.max_height = (max_height == 0 ? monitorSize.height : max_height); + size.max_width = (max_width == 0 ? monitorSize.width : max_width); + size.min_height = min_height; + size.min_width = min_width; + + // On Wayland window manager get the decorators and calculate the differences from the windows' size. + if(onWayland()) + { + if(decoratorWidth == -1 && decoratorHeight == -1) + { + int windowWidth, windowHeight; + gtk_window_get_size(window, &windowWidth, &windowHeight); + + GtkAllocation windowAllocation; + gtk_widget_get_allocation(GTK_WIDGET(window), &windowAllocation); + + decoratorWidth = (windowAllocation.width-windowWidth); + decoratorHeight = (windowAllocation.height-windowHeight); + } + + // Add the decorator difference to the window so fullscreen and maximise can fill the window. + size.max_height = decoratorHeight+size.max_height; + size.max_width = decoratorWidth+size.max_width; + } + + gtk_window_set_geometry_hints(window, NULL, &size, flags); +} + +// function to disable the context menu but propagate the event +static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data) +{ + // return true to disable the context menu + return TRUE; +} + +void DisableContextMenu(void *webview) +{ + // Disable the context menu but propagate the event + g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL); +} + +static gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, void *dummy) +{ + if (event == NULL) + { + xroot = yroot = 0.0f; + dragTime = -1; + return FALSE; + } + mouseButton = event->button; + if (event->button == 3) + { + return FALSE; + } + + if (event->type == GDK_BUTTON_PRESS && event->button == 1) + { + xroot = event->x_root; + yroot = event->y_root; + dragTime = event->time; + } + + return FALSE; +} + +static gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, void *dummy) +{ + if (event == NULL || (event->type == GDK_BUTTON_RELEASE && event->button == 1)) + { + xroot = yroot = 0.0f; + dragTime = -1; + } + return FALSE; +} + +void ConnectButtons(void *webview) +{ + g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-press-event", G_CALLBACK(buttonPress), NULL); + g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-release-event", G_CALLBACK(buttonRelease), NULL); +} + +int IsFullscreen(GtkWidget *widget) +{ + GdkWindow *gdkwindow = gtk_widget_get_window(widget); + GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow)); + return state & GDK_WINDOW_STATE_FULLSCREEN; +} + +int IsMaximised(GtkWidget *widget) +{ + GdkWindow *gdkwindow = gtk_widget_get_window(widget); + GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow)); + return state & GDK_WINDOW_STATE_MAXIMIZED && !(state & GDK_WINDOW_STATE_FULLSCREEN); +} + +int IsMinimised(GtkWidget *widget) +{ + GdkWindow *gdkwindow = gtk_widget_get_window(widget); + GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow)); + return state & GDK_WINDOW_STATE_ICONIFIED; +} + +gboolean Center(gpointer data) +{ + GtkWindow *window = (GtkWindow *)data; + + // Get the geometry of the monitor + GdkRectangle m = getCurrentMonitorGeometry(window); + if (isNULLRectangle(m)) + { + return G_SOURCE_REMOVE; + } + + // Get the window width/height + int windowWidth, windowHeight; + gtk_window_get_size(window, &windowWidth, &windowHeight); + + int newX = ((m.width - windowWidth) / 2) + m.x; + int newY = ((m.height - windowHeight) / 2) + m.y; + + // Place the window at the center of the monitor + gtk_window_move(window, newX, newY); + + return G_SOURCE_REMOVE; +} + +gboolean Show(gpointer data) +{ + gtk_widget_show((GtkWidget *)data); + + return G_SOURCE_REMOVE; +} + +gboolean Hide(gpointer data) +{ + gtk_widget_hide((GtkWidget *)data); + + return G_SOURCE_REMOVE; +} + +gboolean Maximise(gpointer data) +{ + gtk_window_maximize((GtkWindow *)data); + + return G_SOURCE_REMOVE; +} + +gboolean UnMaximise(gpointer data) +{ + gtk_window_unmaximize((GtkWindow *)data); + + return G_SOURCE_REMOVE; +} + +gboolean Minimise(gpointer data) +{ + gtk_window_iconify((GtkWindow *)data); + + return G_SOURCE_REMOVE; +} + +gboolean UnMinimise(gpointer data) +{ + gtk_window_present((GtkWindow *)data); + + return G_SOURCE_REMOVE; +} + +gboolean Fullscreen(gpointer data) +{ + GtkWindow *window = (GtkWindow *)data; + + // Get the geometry of the monitor. + GdkRectangle m = getCurrentMonitorGeometry(window); + if (isNULLRectangle(m)) + { + return G_SOURCE_REMOVE; + } + int scale = getCurrentMonitorScaleFactor(window); + SetMinMaxSize(window, 0, 0, m.width * scale, m.height * scale); + + gtk_window_fullscreen(window); + + return G_SOURCE_REMOVE; +} + +gboolean UnFullscreen(gpointer data) +{ + gtk_window_unfullscreen((GtkWindow *)data); + + return G_SOURCE_REMOVE; +} + +static void webviewLoadChanged(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer data) +{ + if (load_event == WEBKIT_LOAD_FINISHED) + { + processMessage("DomReady"); + } +} + +extern void processURLRequest(void *request); + +// This is called when the close button on the window is pressed +gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void *data) +{ + processMessage("Q"); + // since we handle the close in processMessage tell GTK to not invoke additional handlers - see: + // https://docs.gtk.org/gtk3/signal.Widget.delete-event.html + return TRUE; +} + +char *droppedFiles = NULL; + +static void onDragDataReceived(GtkWidget *self, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, gpointer data) +{ + if(selection_data == NULL || (gtk_selection_data_get_length(selection_data) <= 0) || target_type != 2) + { + return; + } + + if(droppedFiles != NULL) { + free(droppedFiles); + droppedFiles = NULL; + } + + gchar **filenames = NULL; + filenames = g_uri_list_extract_uris((const gchar *)gtk_selection_data_get_data(selection_data)); + if (filenames == NULL) // If unable to retrieve filenames: + { + g_strfreev(filenames); + return; + } + + droppedFiles = calloc((size_t)gtk_selection_data_get_length(selection_data), 1); + + int iter = 0; + while(filenames[iter] != NULL) // The last URI list element is NULL. + { + if(iter != 0) + { + strncat(droppedFiles, "\n", 1); + } + char *filename = g_filename_from_uri(filenames[iter], NULL, NULL); + if (filename == NULL) + { + break; + } + strncat(droppedFiles, filename, strlen(filename)); + + free(filename); + iter++; + } + + g_strfreev(filenames); +} + +static gboolean onDragDrop(GtkWidget* self, GdkDragContext* context, gint x, gint y, guint time, gpointer user_data) +{ + if(droppedFiles == NULL) + { + return FALSE; + } + + size_t resLen = strlen(droppedFiles)+(sizeof(gint)*2)+6; + char *res = calloc(resLen, 1); + + snprintf(res, resLen, "DD:%d:%d:%s", x, y, droppedFiles); + + if(droppedFiles != NULL) { + free(droppedFiles); + droppedFiles = NULL; + } + + processMessage(res); + return FALSE; +} + +// WebView +GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop) +{ + GtkWidget *webview = webkit_web_view_new_with_user_content_manager((WebKitUserContentManager *)contentManager); + + // Store webview reference in the content manager + g_object_set_data(G_OBJECT((WebKitUserContentManager *)contentManager), "webview", webview); + // gtk_container_add(GTK_CONTAINER(window), webview); + WebKitWebContext *context = webkit_web_context_get_default(); + webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL); + g_signal_connect(G_OBJECT(webview), "load-changed", G_CALLBACK(webviewLoadChanged), NULL); + + if(disableWebViewDragAndDrop) + { + gtk_drag_dest_unset(webview); + } + + if(enableDragAndDrop) + { + g_signal_connect(G_OBJECT(webview), "drag-data-received", G_CALLBACK(onDragDataReceived), NULL); + g_signal_connect(G_OBJECT(webview), "drag-drop", G_CALLBACK(onDragDrop), NULL); + } + + if (hideWindowOnClose) + { + g_signal_connect(GTK_WIDGET(window), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); + } + else + { + g_signal_connect(GTK_WIDGET(window), "delete-event", G_CALLBACK(close_button_pressed), NULL); + } + + WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview)); + webkit_settings_set_user_agent_with_application_details(settings, "wails.io", ""); + + switch (gpuPolicy) + { + case 0: + webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); + break; + case 1: + webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND); + break; + case 2: + webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER); + break; + default: + webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND); + } + return webview; +} + +void DevtoolsEnabled(void *webview, int enabled, bool showInspector) +{ + WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview)); + gboolean genabled = enabled == 1 ? true : false; + webkit_settings_set_enable_developer_extras(settings, genabled); + + if (genabled && showInspector) + { + ShowInspector(webview); + } +} + +void LoadIndex(void *webview, char *url) +{ + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url); +} + +static gboolean startDrag(gpointer data) +{ + DragOptions *options = (DragOptions *)data; + + // Ignore non-toplevel widgets + GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(options->webview)); + if (!GTK_IS_WINDOW(window)) + { + free(data); + return G_SOURCE_REMOVE; + } + + gtk_window_begin_move_drag(options->mainwindow, mouseButton, xroot, yroot, dragTime); + free(data); + + return G_SOURCE_REMOVE; +} + +void StartDrag(void *webview, GtkWindow *mainwindow) +{ + DragOptions *data = malloc(sizeof(DragOptions)); + data->webview = webview; + data->mainwindow = mainwindow; + ExecuteOnMainThread(startDrag, (gpointer)data); +} + +static gboolean startResize(gpointer data) +{ + ResizeOptions *options = (ResizeOptions *)data; + + // Ignore non-toplevel widgets + GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(options->webview)); + if (!GTK_IS_WINDOW(window)) + { + free(data); + return G_SOURCE_REMOVE; + } + + gtk_window_begin_resize_drag(options->mainwindow, options->edge, mouseButton, xroot, yroot, dragTime); + free(data); + + return G_SOURCE_REMOVE; +} + +void StartResize(void *webview, GtkWindow *mainwindow, GdkWindowEdge edge) +{ + ResizeOptions *data = malloc(sizeof(ResizeOptions)); + data->webview = webview; + data->mainwindow = mainwindow; + data->edge = edge; + ExecuteOnMainThread(startResize, (gpointer)data); +} + +void ExecuteJS(void *data) +{ + struct JSCallback *js = data; + webkit_web_view_run_javascript(js->webview, js->script, NULL, NULL, NULL); + free(js->script); +} + +void extern processMessageDialogResult(char *); + +void MessageDialog(void *data) +{ + GtkDialogFlags flags; + GtkMessageType messageType; + MessageDialogOptions *options = (MessageDialogOptions *)data; + if (options->messageType == 0) + { + messageType = GTK_MESSAGE_INFO; + flags = GTK_BUTTONS_OK; + } + else if (options->messageType == 1) + { + messageType = GTK_MESSAGE_ERROR; + flags = GTK_BUTTONS_OK; + } + else if (options->messageType == 2) + { + messageType = GTK_MESSAGE_QUESTION; + flags = GTK_BUTTONS_YES_NO; + } + else + { + messageType = GTK_MESSAGE_WARNING; + flags = GTK_BUTTONS_OK; + } + + GtkWidget *dialog; + dialog = gtk_message_dialog_new(GTK_WINDOW(options->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + messageType, + flags, + options->message, NULL); + gtk_window_set_title(GTK_WINDOW(dialog), options->title); + GtkResponseType result = gtk_dialog_run(GTK_DIALOG(dialog)); + if (result == GTK_RESPONSE_YES) + { + processMessageDialogResult("Yes"); + } + else if (result == GTK_RESPONSE_NO) + { + processMessageDialogResult("No"); + } + else if (result == GTK_RESPONSE_OK) + { + processMessageDialogResult("OK"); + } + else if (result == GTK_RESPONSE_CANCEL) + { + processMessageDialogResult("Cancel"); + } + else + { + processMessageDialogResult(""); + } + + gtk_widget_destroy(dialog); + free(options->title); + free(options->message); +} + +void extern processOpenFileResult(void *); + +GtkFileFilter **AllocFileFilterArray(size_t ln) +{ + return (GtkFileFilter **)malloc(ln * sizeof(GtkFileFilter *)); +} + +void freeFileFilterArray(GtkFileFilter **filters) +{ + free(filters); +} + +void Opendialog(void *data) +{ + struct OpenFileDialogOptions *options = data; + char *label = "_Open"; + if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE) + { + label = "_Save"; + } + GtkWidget *dlgWidget = gtk_file_chooser_dialog_new(options->title, options->window, options->action, + "_Cancel", GTK_RESPONSE_CANCEL, + label, GTK_RESPONSE_ACCEPT, + NULL); + + GtkFileChooser *fc = GTK_FILE_CHOOSER(dlgWidget); + // filters + if (options->filters != 0) + { + int index = 0; + GtkFileFilter *thisFilter; + while (options->filters[index] != NULL) + { + thisFilter = options->filters[index]; + gtk_file_chooser_add_filter(fc, thisFilter); + index++; + } + } + + gtk_file_chooser_set_local_only(fc, FALSE); + + if (options->multipleFiles == 1) + { + gtk_file_chooser_set_select_multiple(fc, TRUE); + } + gtk_file_chooser_set_do_overwrite_confirmation(fc, TRUE); + if (options->createDirectories == 1) + { + gtk_file_chooser_set_create_folders(fc, TRUE); + } + if (options->showHiddenFiles == 1) + { + gtk_file_chooser_set_show_hidden(fc, TRUE); + } + + if (options->defaultDirectory != NULL) + { + gtk_file_chooser_set_current_folder(fc, options->defaultDirectory); + free(options->defaultDirectory); + } + + if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE) + { + if (options->defaultFilename != NULL) + { + gtk_file_chooser_set_current_name(fc, options->defaultFilename); + free(options->defaultFilename); + } + } + + gint response = gtk_dialog_run(GTK_DIALOG(dlgWidget)); + + // Max 1024 files to select + char **result = calloc(1024, sizeof(char *)); + int resultIndex = 0; + + if (response == GTK_RESPONSE_ACCEPT) + { + GSList *filenames = gtk_file_chooser_get_filenames(fc); + GSList *iter = filenames; + while (iter) + { + result[resultIndex++] = (char *)iter->data; + iter = g_slist_next(iter); + if (resultIndex == 1024) + { + break; + } + } + processOpenFileResult(result); + iter = filenames; + while (iter) + { + g_free(iter->data); + iter = g_slist_next(iter); + } + } + else + { + processOpenFileResult(result); + } + free(result); + + // Release filters + if (options->filters != NULL) + { + int index = 0; + GtkFileFilter *thisFilter; + while (options->filters[index] != 0) + { + thisFilter = options->filters[index]; + g_object_unref(thisFilter); + index++; + } + freeFileFilterArray(options->filters); + } + gtk_widget_destroy(dlgWidget); + free(options->title); +} + +GtkFileFilter *newFileFilter() +{ + GtkFileFilter *result = gtk_file_filter_new(); + g_object_ref(result); + return result; +} + +void ShowInspector(void *webview) { + WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(webview)); + webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); +} + +void sendShowInspectorMessage() { + processMessage("wails:showInspector"); +} + +void InstallF12Hotkey(void *window) +{ + // When the user presses Ctrl+Shift+F12, call ShowInspector + GtkAccelGroup *accel_group = gtk_accel_group_new(); + gtk_window_add_accel_group(GTK_WINDOW(window), accel_group); + GClosure *closure = g_cclosure_new(G_CALLBACK(sendShowInspectorMessage), window, NULL); + gtk_accel_group_connect(accel_group, GDK_KEY_F12, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE, closure); +} diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go new file mode 100644 index 000000000..0bf5ac51d --- /dev/null +++ b/v2/internal/frontend/desktop/linux/window.go @@ -0,0 +1,479 @@ +//go:build linux +// +build linux + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include +#include +#include +#include +#include +#include +#include "window.h" + +*/ +import "C" +import ( + "log" + "strings" + "sync" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/linux" +) + +func gtkBool(input bool) C.gboolean { + if input { + return C.gboolean(1) + } + return C.gboolean(0) +} + +type Window struct { + appoptions *options.App + debug bool + devtoolsEnabled bool + gtkWindow unsafe.Pointer + contentManager unsafe.Pointer + webview unsafe.Pointer + applicationMenu *menu.Menu + menubar *C.GtkWidget + webviewBox *C.GtkWidget + vbox *C.GtkWidget + accels *C.GtkAccelGroup + minWidth, minHeight, maxWidth, maxHeight int +} + +func bool2Cint(value bool) C.int { + if value { + return C.int(1) + } + return C.int(0) +} + +func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Window { + validateWebKit2Version(appoptions) + + result := &Window{ + appoptions: appoptions, + debug: debug, + devtoolsEnabled: devtoolsEnabled, + minHeight: appoptions.MinHeight, + minWidth: appoptions.MinWidth, + maxHeight: appoptions.MaxHeight, + maxWidth: appoptions.MaxWidth, + } + + gtkWindow := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL) + C.g_object_ref_sink(C.gpointer(gtkWindow)) + result.gtkWindow = unsafe.Pointer(gtkWindow) + + webviewName := C.CString("webview-box") + defer C.free(unsafe.Pointer(webviewName)) + result.webviewBox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) + C.gtk_widget_set_name(result.webviewBox, webviewName) + + result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) + C.gtk_container_add(result.asGTKContainer(), result.vbox) + + result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new()) + external := C.CString("external") + defer C.free(unsafe.Pointer(external)) + C.webkit_user_content_manager_register_script_message_handler(result.cWebKitUserContentManager(), external) + C.SetupInvokeSignal(result.contentManager) + + var webviewGpuPolicy int + if appoptions.Linux != nil { + webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy) + } else { + // workaround for https://github.com/wailsapp/wails/issues/2977 + webviewGpuPolicy = int(linux.WebviewGpuPolicyNever) + } + + webview := C.SetupWebview( + result.contentManager, + result.asGTKWindow(), + bool2Cint(appoptions.HideWindowOnClose), + C.int(webviewGpuPolicy), + bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.DisableWebViewDrop), + bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.EnableFileDrop), + ) + result.webview = unsafe.Pointer(webview) + buttonPressedName := C.CString("button-press-event") + defer C.free(unsafe.Pointer(buttonPressedName)) + C.ConnectButtons(unsafe.Pointer(webview)) + + if devtoolsEnabled { + C.DevtoolsEnabled(unsafe.Pointer(webview), C.int(1), C.bool(debug && appoptions.Debug.OpenInspectorOnStartup)) + // Install Ctrl-Shift-F12 hotkey to call ShowInspector + C.InstallF12Hotkey(unsafe.Pointer(gtkWindow)) + } + + if !(debug || appoptions.EnableDefaultContextMenu) { + C.DisableContextMenu(unsafe.Pointer(webview)) + } + + // Set background colour + RGBA := appoptions.BackgroundColour + result.SetBackgroundColour(RGBA.R, RGBA.G, RGBA.B, RGBA.A) + + // Setup window + result.SetKeepAbove(appoptions.AlwaysOnTop) + result.SetResizable(!appoptions.DisableResize) + result.SetDefaultSize(appoptions.Width, appoptions.Height) + result.SetDecorated(!appoptions.Frameless) + result.SetTitle(appoptions.Title) + result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight) + result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) + if appoptions.Linux != nil { + if appoptions.Linux.Icon != nil { + result.SetWindowIcon(appoptions.Linux.Icon) + } + if appoptions.Linux.WindowIsTranslucent { + C.SetWindowTransparency(gtkWindow) + } + } + + // Menu + result.SetApplicationMenu(appoptions.Menu) + + return result +} + +func (w *Window) asGTKWidget() *C.GtkWidget { + return C.GTKWIDGET(w.gtkWindow) +} + +func (w *Window) asGTKWindow() *C.GtkWindow { + return C.GTKWINDOW(w.gtkWindow) +} + +func (w *Window) asGTKContainer() *C.GtkContainer { + return C.GTKCONTAINER(w.gtkWindow) +} + +func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager { + return (*C.WebKitUserContentManager)(w.contentManager) +} + +func (w *Window) Fullscreen() { + C.ExecuteOnMainThread(C.Fullscreen, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) UnFullscreen() { + if !w.IsFullScreen() { + return + } + C.ExecuteOnMainThread(C.UnFullscreen, C.gpointer(w.asGTKWindow())) + w.SetMinSize(w.minWidth, w.minHeight) + w.SetMaxSize(w.maxWidth, w.maxHeight) +} + +func (w *Window) Destroy() { + C.gtk_widget_destroy(w.asGTKWidget()) + C.g_object_unref(C.gpointer(w.gtkWindow)) +} + +func (w *Window) Close() { + C.gtk_window_close(w.asGTKWindow()) +} + +func (w *Window) Center() { + C.ExecuteOnMainThread(C.Center, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) SetPosition(x int, y int) { + invokeOnMainThread(func() { + C.SetPosition(unsafe.Pointer(w.asGTKWindow()), C.int(x), C.int(y)) + }) +} + +func (w *Window) Size() (int, int) { + var width, height C.int + var wg sync.WaitGroup + wg.Add(1) + invokeOnMainThread(func() { + C.gtk_window_get_size(w.asGTKWindow(), &width, &height) + wg.Done() + }) + wg.Wait() + return int(width), int(height) +} + +func (w *Window) GetPosition() (int, int) { + var width, height C.int + var wg sync.WaitGroup + wg.Add(1) + invokeOnMainThread(func() { + C.gtk_window_get_position(w.asGTKWindow(), &width, &height) + wg.Done() + }) + wg.Wait() + return int(width), int(height) +} + +func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { + w.maxHeight = maxHeight + w.maxWidth = maxWidth + invokeOnMainThread(func() { + C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight)) + }) +} + +func (w *Window) SetMinSize(minWidth int, minHeight int) { + w.minHeight = minHeight + w.minWidth = minWidth + invokeOnMainThread(func() { + C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight)) + }) +} + +func (w *Window) Show() { + C.ExecuteOnMainThread(C.Show, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) Hide() { + C.ExecuteOnMainThread(C.Hide, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) Maximise() { + C.ExecuteOnMainThread(C.Maximise, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) UnMaximise() { + C.ExecuteOnMainThread(C.UnMaximise, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) Minimise() { + C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) UnMinimise() { + C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow())) +} + +func (w *Window) IsFullScreen() bool { + result := C.IsFullscreen(w.asGTKWidget()) + if result != 0 { + return true + } + return false +} + +func (w *Window) IsMaximised() bool { + result := C.IsMaximised(w.asGTKWidget()) + return result > 0 +} + +func (w *Window) IsMinimised() bool { + result := C.IsMinimised(w.asGTKWidget()) + return result > 0 +} + +func (w *Window) IsNormal() bool { + return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen() +} + +func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) { + windowIsTranslucent := false + if w.appoptions.Linux != nil && w.appoptions.Linux.WindowIsTranslucent { + windowIsTranslucent = true + } + data := C.RGBAOptions{ + r: C.uchar(r), + g: C.uchar(g), + b: C.uchar(b), + a: C.uchar(a), + webview: w.webview, + webviewBox: unsafe.Pointer(w.webviewBox), + windowIsTranslucent: gtkBool(windowIsTranslucent), + } + invokeOnMainThread(func() { C.SetBackgroundColour(unsafe.Pointer(&data)) }) + +} + +func (w *Window) SetWindowIcon(icon []byte) { + if len(icon) == 0 { + return + } + C.SetWindowIcon(w.asGTKWindow(), (*C.guchar)(&icon[0]), (C.gsize)(len(icon))) +} + +func (w *Window) Run(url string) { + if w.menubar != nil { + C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar, 0, 0, 0) + } + + C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.webviewBox)), C.GTKWIDGET(w.webview), 1, 1, 0) + C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.webviewBox, 1, 1, 0) + _url := C.CString(url) + C.LoadIndex(w.webview, _url) + defer C.free(unsafe.Pointer(_url)) + if w.appoptions.StartHidden { + w.Hide() + } + C.gtk_widget_show_all(w.asGTKWidget()) + w.Center() + switch w.appoptions.WindowStartState { + case options.Fullscreen: + w.Fullscreen() + case options.Minimised: + w.Minimise() + case options.Maximised: + w.Maximise() + } +} + +func (w *Window) SetKeepAbove(top bool) { + C.gtk_window_set_keep_above(w.asGTKWindow(), gtkBool(top)) +} + +func (w *Window) SetResizable(resizable bool) { + C.gtk_window_set_resizable(w.asGTKWindow(), gtkBool(resizable)) +} + +func (w *Window) SetDefaultSize(width int, height int) { + C.gtk_window_set_default_size(w.asGTKWindow(), C.int(width), C.int(height)) +} + +func (w *Window) SetSize(width int, height int) { + C.gtk_window_resize(w.asGTKWindow(), C.gint(width), C.gint(height)) +} + +func (w *Window) SetDecorated(frameless bool) { + C.gtk_window_set_decorated(w.asGTKWindow(), gtkBool(frameless)) +} + +func (w *Window) SetTitle(title string) { + C.SetTitle(w.asGTKWindow(), C.CString(title)) +} + +func (w *Window) ExecJS(js string) { + jscallback := C.JSCallback{ + webview: w.webview, + script: C.CString(js), + } + invokeOnMainThread(func() { C.ExecuteJS(unsafe.Pointer(&jscallback)) }) +} + +func (w *Window) StartDrag() { + C.StartDrag(w.webview, w.asGTKWindow()) +} + +func (w *Window) StartResize(edge uintptr) { + C.StartResize(w.webview, w.asGTKWindow(), C.GdkWindowEdge(edge)) +} + +func (w *Window) Quit() { + C.gtk_main_quit() +} + +func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) { + + data := C.OpenFileDialogOptions{ + window: w.asGTKWindow(), + title: C.CString(dialogOptions.Title), + multipleFiles: C.int(multipleFiles), + action: action, + } + + if len(dialogOptions.Filters) > 0 { + // Create filter array + mem := NewCalloc() + arraySize := len(dialogOptions.Filters) + 1 + data.filters = C.AllocFileFilterArray((C.size_t)(arraySize)) + filters := unsafe.Slice((**C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)), arraySize) + for index, filter := range dialogOptions.Filters { + thisFilter := C.gtk_file_filter_new() + C.g_object_ref(C.gpointer(thisFilter)) + if filter.DisplayName != "" { + cName := mem.String(filter.DisplayName) + C.gtk_file_filter_set_name(thisFilter, cName) + } + if filter.Pattern != "" { + for _, thisPattern := range strings.Split(filter.Pattern, ";") { + cThisPattern := mem.String(thisPattern) + C.gtk_file_filter_add_pattern(thisFilter, cThisPattern) + } + } + // Add filter to array + filters[index] = thisFilter + } + mem.Free() + filters[arraySize-1] = nil + } + + if dialogOptions.CanCreateDirectories { + data.createDirectories = C.int(1) + } + + if dialogOptions.ShowHiddenFiles { + data.showHiddenFiles = C.int(1) + } + + if dialogOptions.DefaultFilename != "" { + data.defaultFilename = C.CString(dialogOptions.DefaultFilename) + } + + if dialogOptions.DefaultDirectory != "" { + data.defaultDirectory = C.CString(dialogOptions.DefaultDirectory) + } + + invokeOnMainThread(func() { C.Opendialog(unsafe.Pointer(&data)) }) +} + +func (w *Window) MessageDialog(dialogOptions frontend.MessageDialogOptions) { + + data := C.MessageDialogOptions{ + window: w.gtkWindow, + title: C.CString(dialogOptions.Title), + message: C.CString(dialogOptions.Message), + } + switch dialogOptions.Type { + case frontend.InfoDialog: + data.messageType = C.int(0) + case frontend.ErrorDialog: + data.messageType = C.int(1) + case frontend.QuestionDialog: + data.messageType = C.int(2) + case frontend.WarningDialog: + data.messageType = C.int(3) + } + invokeOnMainThread(func() { C.MessageDialog(unsafe.Pointer(&data)) }) +} + +func (w *Window) ToggleMaximise() { + if w.IsMaximised() { + w.UnMaximise() + } else { + w.Maximise() + } +} + +func (w *Window) ShowInspector() { + invokeOnMainThread(func() { C.ShowInspector(w.webview) }) +} + +// showModalDialogAndExit shows a modal dialog and exits the app. +func showModalDialogAndExit(title, message string) { + go func() { + data := C.MessageDialogOptions{ + title: C.CString(title), + message: C.CString(message), + messageType: C.int(1), + } + + C.MessageDialog(unsafe.Pointer(&data)) + }() + + <-messageDialogResult + log.Fatal(message) +} diff --git a/v2/internal/frontend/desktop/linux/window.h b/v2/internal/frontend/desktop/linux/window.h new file mode 100644 index 000000000..04410959a --- /dev/null +++ b/v2/internal/frontend/desktop/linux/window.h @@ -0,0 +1,128 @@ +#ifndef window_h +#define window_h + +#include +#include +#include +#include +#include +#include + +typedef struct DragOptions +{ + void *webview; + GtkWindow *mainwindow; +} DragOptions; + +typedef struct ResizeOptions +{ + void *webview; + GtkWindow *mainwindow; + GdkWindowEdge edge; +} ResizeOptions; + +typedef struct JSCallback +{ + void *webview; + char *script; +} JSCallback; + +typedef struct MessageDialogOptions +{ + void *window; + char *title; + char *message; + int messageType; +} MessageDialogOptions; + +typedef struct OpenFileDialogOptions +{ + GtkWindow *window; + char *title; + char *defaultFilename; + char *defaultDirectory; + int createDirectories; + int multipleFiles; + int showHiddenFiles; + GtkFileChooserAction action; + GtkFileFilter **filters; +} OpenFileDialogOptions; + +typedef struct RGBAOptions +{ + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + void *webview; + void *webviewBox; + gboolean windowIsTranslucent; +} RGBAOptions; + +typedef struct SetTitleArgs +{ + GtkWindow *window; + char *title; +} SetTitleArgs; + +typedef struct SetPositionArgs +{ + int x; + int y; + void *window; +} SetPositionArgs; + +void ExecuteOnMainThread(void *f, gpointer jscallback); + +GtkWidget *GTKWIDGET(void *pointer); +GtkWindow *GTKWINDOW(void *pointer); +GtkContainer *GTKCONTAINER(void *pointer); +GtkBox *GTKBOX(void *pointer); + +// window +ulong SetupInvokeSignal(void *contentManager); + +void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len); +void SetWindowTransparency(GtkWidget *widget); +void SetBackgroundColour(void *data); +void SetTitle(GtkWindow *window, char *title); +void SetPosition(void *window, int x, int y); +void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height); +void DisableContextMenu(void *webview); +void ConnectButtons(void *webview); + +int IsFullscreen(GtkWidget *widget); +int IsMaximised(GtkWidget *widget); +int IsMinimised(GtkWidget *widget); + +gboolean Center(gpointer data); +gboolean Show(gpointer data); +gboolean Hide(gpointer data); +gboolean Maximise(gpointer data); +gboolean UnMaximise(gpointer data); +gboolean Minimise(gpointer data); +gboolean UnMinimise(gpointer data); +gboolean Fullscreen(gpointer data); +gboolean UnFullscreen(gpointer data); + +// WebView +GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop); +void LoadIndex(void *webview, char *url); +void DevtoolsEnabled(void *webview, int enabled, bool showInspector); +void ExecuteJS(void *data); + +// Drag +void StartDrag(void *webview, GtkWindow *mainwindow); +void StartResize(void *webview, GtkWindow *mainwindow, GdkWindowEdge edge); + +// Dialog +void MessageDialog(void *data); +GtkFileFilter **AllocFileFilterArray(size_t ln); +void Opendialog(void *data); + +// Inspector +void sendShowInspectorMessage(); +void ShowInspector(void *webview); +void InstallF12Hotkey(void *window); + +#endif /* window_h */ diff --git a/v2/internal/frontend/desktop/windows/browser.go b/v2/internal/frontend/desktop/windows/browser.go new file mode 100644 index 000000000..13d037b14 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/browser.go @@ -0,0 +1,43 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" + "golang.org/x/sys/windows" +) + +var fallbackBrowserPaths = []string{ + `\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, + `\Program Files\Google\Chrome\Application\chrome.exe`, + `\Program Files (x86)\Google\Chrome\Application\chrome.exe`, + `\Program Files\Mozilla Firefox\firefox.exe`, +} + +// BrowserOpenURL Use the default browser to open the url +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } + + // Specific method implementation + err = browser.OpenURL(url) + if err == nil { + return + } + for _, fallback := range fallbackBrowserPaths { + if err := openBrowser(fallback, url); err == nil { + return + } + } + f.logger.Error("Unable to open default system browser") +} + +func openBrowser(path, url string) error { + return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(path), windows.StringToUTF16Ptr(url), nil, windows.SW_SHOWNORMAL) +} diff --git a/v2/internal/frontend/desktop/windows/clipboard.go b/v2/internal/frontend/desktop/windows/clipboard.go new file mode 100644 index 000000000..975fa8e7c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/clipboard.go @@ -0,0 +1,16 @@ +//go:build windows +// +build windows + +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" +) + +func (f *Frontend) ClipboardGetText() (string, error) { + return win32.GetClipboardText() +} + +func (f *Frontend) ClipboardSetText(text string) error { + return win32.SetClipboardText(text) +} diff --git a/v2/internal/frontend/desktop/windows/dialog.go b/v2/internal/frontend/desktop/windows/dialog.go new file mode 100644 index 000000000..573325886 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/dialog.go @@ -0,0 +1,210 @@ +//go:build windows +// +build windows + +package windows + +import ( + "path/filepath" + "strings" + "syscall" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" + "golang.org/x/sys/windows" +) + +func (f *Frontend) getHandleForDialog() w32.HWND { + if f.mainWindow.IsVisible() { + return f.mainWindow.Handle() + } + return 0 +} + +func getDefaultFolder(folder string) (string, error) { + if folder == "" { + return "", nil + } + return filepath.Abs(folder) +} + +// OpenDirectoryDialog prompts the user to select a directory +func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (string, error) { + + defaultFolder, err := getDefaultFolder(options.DefaultDirectory) + if err != nil { + return "", err + } + + config := cfd.DialogConfig{ + Title: options.Title, + Role: "PickFolder", + Folder: defaultFolder, + } + + result, err := f.showCfdDialog( + func() (cfd.Dialog, error) { + return cfd.NewSelectFolderDialog(config) + }, false) + + if err != nil && err != cfd.ErrCancelled { + return "", err + } + return result.(string), nil +} + +// OpenFileDialog prompts the user to select a file +func (f *Frontend) OpenFileDialog(options frontend.OpenDialogOptions) (string, error) { + defaultFolder, err := getDefaultFolder(options.DefaultDirectory) + if err != nil { + return "", err + } + + config := cfd.DialogConfig{ + Folder: defaultFolder, + FileFilters: convertFilters(options.Filters), + FileName: options.DefaultFilename, + Title: options.Title, + } + + result, err := f.showCfdDialog( + func() (cfd.Dialog, error) { + return cfd.NewOpenFileDialog(config) + }, false) + + if err != nil && err != cfd.ErrCancelled { + return "", err + } + return result.(string), nil +} + +// OpenMultipleFilesDialog prompts the user to select a file +func (f *Frontend) OpenMultipleFilesDialog(options frontend.OpenDialogOptions) ([]string, error) { + + defaultFolder, err := getDefaultFolder(options.DefaultDirectory) + if err != nil { + return nil, err + } + + config := cfd.DialogConfig{ + Title: options.Title, + Role: "OpenMultipleFiles", + FileFilters: convertFilters(options.Filters), + FileName: options.DefaultFilename, + Folder: defaultFolder, + } + + result, err := f.showCfdDialog( + func() (cfd.Dialog, error) { + return cfd.NewOpenMultipleFilesDialog(config) + }, true) + + if err != nil && err != cfd.ErrCancelled { + return nil, err + } + return result.([]string), nil +} + +// SaveFileDialog prompts the user to select a file +func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, error) { + + defaultFolder, err := getDefaultFolder(options.DefaultDirectory) + if err != nil { + return "", err + } + + config := cfd.DialogConfig{ + Title: options.Title, + Role: "SaveFile", + FileFilters: convertFilters(options.Filters), + FileName: options.DefaultFilename, + Folder: defaultFolder, + } + + if len(options.Filters) > 0 { + config.DefaultExtension = strings.TrimPrefix(strings.Split(options.Filters[0].Pattern, ";")[0], "*") + } + + result, err := f.showCfdDialog( + func() (cfd.Dialog, error) { + return cfd.NewSaveFileDialog(config) + }, false) + + if err != nil && err != cfd.ErrCancelled { + return "", err + } + return result.(string), nil +} + +func (f *Frontend) showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool) (any, error) { + return invokeSync(f.mainWindow, func() (any, error) { + dlg, err := newDlg() + if err != nil { + return nil, err + } + defer func() { + err := dlg.Release() + if err != nil { + println("ERROR: Unable to release dialog:", err.Error()) + } + }() + + dlg.SetParentWindowHandle(f.getHandleForDialog()) + if multi, _ := dlg.(cfd.OpenMultipleFilesDialog); multi != nil && isMultiSelect { + return multi.ShowAndGetResults() + } + return dlg.ShowAndGetResult() + }) +} + +func calculateMessageDialogFlags(options frontend.MessageDialogOptions) uint32 { + var flags uint32 + + switch options.Type { + case frontend.InfoDialog: + flags = windows.MB_OK | windows.MB_ICONINFORMATION + case frontend.ErrorDialog: + flags = windows.MB_ICONERROR | windows.MB_OK + case frontend.QuestionDialog: + flags = windows.MB_YESNO + if strings.TrimSpace(strings.ToLower(options.DefaultButton)) == "no" { + flags |= windows.MB_DEFBUTTON2 + } + case frontend.WarningDialog: + flags = windows.MB_OK | windows.MB_ICONWARNING + } + + return flags +} + +// MessageDialog show a message dialog to the user +func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, error) { + + title, err := syscall.UTF16PtrFromString(options.Title) + if err != nil { + return "", err + } + message, err := syscall.UTF16PtrFromString(options.Message) + if err != nil { + return "", err + } + + flags := calculateMessageDialogFlags(options) + + button, _ := windows.MessageBox(windows.HWND(f.getHandleForDialog()), message, title, flags|windows.MB_SYSTEMMODAL) + // This maps MessageBox return values to strings + responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"} + result := "Error" + if int(button) < len(responses) { + result = responses[button] + } + return result, nil +} + +func convertFilters(filters []frontend.FileFilter) []cfd.FileFilter { + var result []cfd.FileFilter + for _, filter := range filters { + result = append(result, cfd.FileFilter(filter)) + } + return result +} diff --git a/v2/internal/frontend/desktop/windows/dialog_test.go b/v2/internal/frontend/desktop/windows/dialog_test.go new file mode 100644 index 000000000..e91058e92 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/dialog_test.go @@ -0,0 +1,77 @@ +//go:build windows + +package windows + +import ( + "testing" + + "github.com/wailsapp/wails/v2/internal/frontend" + "golang.org/x/sys/windows" +) + +func Test_calculateMessageDialogFlags(t *testing.T) { + tests := []struct { + name string + options frontend.MessageDialogOptions + want uint32 + }{ + { + name: "Test Info Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.InfoDialog, + }, + want: windows.MB_OK | windows.MB_ICONINFORMATION, + }, + { + name: "Test Error Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.ErrorDialog, + }, + want: windows.MB_ICONERROR | windows.MB_OK, + }, + { + name: "Test Question Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + }, + want: windows.MB_YESNO, + }, + { + name: "Test Question Dialog with default cancel", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + DefaultButton: "No", + }, + want: windows.MB_YESNO | windows.MB_DEFBUTTON2, + }, + { + name: "Test Question Dialog with default cancel (lowercase)", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + DefaultButton: "no", + }, + want: windows.MB_YESNO | windows.MB_DEFBUTTON2, + }, + { + name: "Test Warning Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.WarningDialog, + }, + want: windows.MB_OK | windows.MB_ICONWARNING, + }, + { + name: "Test Error Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.ErrorDialog, + }, + want: windows.MB_ICONERROR | windows.MB_OK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateMessageDialogFlags(tt.options); got != tt.want { + t.Errorf("calculateMessageDialogFlags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go new file mode 100644 index 000000000..5df13ed98 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -0,0 +1,1004 @@ +//go:build windows +// +build windows + +package windows + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net" + "net/url" + "os" + "runtime" + "strings" + "sync" + "text/template" + "time" + "unsafe" + + "github.com/bep/debounce" + "github.com/wailsapp/go-webview2/pkg/edge" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" + wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/wailsapp/wails/v2/internal/logger" + w32consts "github.com/wailsapp/wails/v2/internal/platform/win32" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "github.com/wailsapp/wails/v2/pkg/assetserver" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/windows" + w "golang.org/x/sys/windows" +) + +const startURL = "http://wails.localhost/" + +var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) + +type Screen = frontend.Screen + +type Frontend struct { + + // Context + ctx context.Context + + frontendOptions *options.App + logger *logger.Logger + chromium *edge.Chromium + debug bool + devtoolsEnabled bool + + // Assets + assets *assetserver.AssetServer + startURL *url.URL + + // main window handle + mainWindow *Window + bindings *binding.Bindings + dispatcher frontend.Dispatcher + + hasStarted bool + + originValidator *originvalidator.OriginValidator + + // Windows build number + versionInfo *operatingsystem.WindowsVersionInfo + resizeDebouncer func(f func()) +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + + // Get Windows build number + versionInfo, _ := operatingsystem.GetWindowsVersionInfo() + + // Apply DLL search path settings if specified + if appoptions.Windows != nil && appoptions.Windows.DLLSearchPaths != 0 { + w.SetDefaultDllDirectories(appoptions.Windows.DLLSearchPaths) + } + // Now initialize packages that load DLLs + w32.Init() + w32consts.Init() + result := &Frontend{ + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + versionInfo: versionInfo, + } + + if appoptions.Windows != nil { + if appoptions.Windows.ResizeDebounceMS > 0 { + result.resizeDebouncer = debounce.New(time.Duration(appoptions.Windows.ResizeDebounceMS) * time.Millisecond) + } + } + + // We currently can't use wails://wails/ as other platforms do, therefore we map the assets sever onto the following url. + result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + + if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { + result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + return result + } + + if port, _ := ctx.Value("assetserverport").(string); port != "" { + result.startURL.Host = net.JoinHostPort(result.startURL.Host, port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + } + + var bindings string + var err error + if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated { + bindings, err = appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + } else { + appBindings.DB().UpdateObfuscatedCallMap() + } + + assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, wailsruntime.RuntimeAssetsBundle) + if err != nil { + log.Fatal(err) + } + result.assets = assets + + go result.startSecondInstanceProcessor() + + return result +} + +func (f *Frontend) WindowReload() { + f.ExecJS("runtime.WindowReload();") +} + +func (f *Frontend) WindowSetSystemDefaultTheme() { + f.mainWindow.SetTheme(windows.SystemDefault) +} + +func (f *Frontend) WindowSetLightTheme() { + f.mainWindow.SetTheme(windows.Light) +} + +func (f *Frontend) WindowSetDarkTheme() { + f.mainWindow.SetTheme(windows.Dark) +} + +func (f *Frontend) Run(ctx context.Context) error { + f.ctx = ctx + + f.chromium = edge.NewChromium() + + if f.frontendOptions.SingleInstanceLock != nil { + SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + + mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) + f.mainWindow = mainWindow + + var _debug = ctx.Value("debug") + var _devtoolsEnabled = ctx.Value("devtoolsEnabled") + + if _debug != nil { + f.debug = _debug.(bool) + } + if _devtoolsEnabled != nil { + f.devtoolsEnabled = _devtoolsEnabled.(bool) + } + + f.WindowCenter() + f.setupChromium() + + mainWindow.OnSize().Bind(func(arg *winc.Event) { + if f.frontendOptions.Frameless { + // If the window is frameless and we are minimizing, then we need to suppress the Resize on the + // WebView2. If we don't do this, restoring does not work as expected and first restores with some wrong + // size during the restore animation and only fully renders when the animation is done. This highly + // depends on the content in the WebView, see https://github.com/wailsapp/wails/issues/1319 + event, _ := arg.Data.(*winc.SizeEventData) + if event != nil && event.Type == w32.SIZE_MINIMIZED { + // Set minimizing flag to prevent unnecessary redraws during minimize/restore for frameless windows + // 设置最小化标志以防止无边框窗口在最小化/恢复过程中的不必要重绘 + // This fixes window flickering when minimizing/restoring frameless windows + // 这修复了无边框窗口在最小化/恢复时的闪烁问题 + // Reference: https://github.com/wailsapp/wails/issues/3951 + f.mainWindow.isMinimizing = true + return + } + } + + // Clear minimizing flag for all non-minimize size events + // 对于所有非最小化的尺寸变化事件,清除最小化标志 + // Reference: https://github.com/wailsapp/wails/issues/3951 + f.mainWindow.isMinimizing = false + + if f.resizeDebouncer != nil { + f.resizeDebouncer(func() { + f.mainWindow.Invoke(func() { + f.chromium.Resize() + }) + }) + } else { + f.chromium.Resize() + } + }) + + mainWindow.OnClose().Bind(func(arg *winc.Event) { + if f.frontendOptions.HideWindowOnClose { + f.WindowHide() + } else { + f.Quit() + } + }) + + go func() { + if f.frontendOptions.OnStartup != nil { + f.frontendOptions.OnStartup(f.ctx) + } + }() + mainWindow.UpdateTheme() + return nil +} + +func (f *Frontend) WindowClose() { + if f.mainWindow != nil { + f.mainWindow.Close() + } +} + +func (f *Frontend) RunMainLoop() { + _ = winc.RunMainLoop() +} + +func (f *Frontend) WindowCenter() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.Center() +} + +func (f *Frontend) WindowSetAlwaysOnTop(b bool) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetAlwaysOnTop(b) +} + +func (f *Frontend) WindowSetPosition(x, y int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetPos(x, y) +} +func (f *Frontend) WindowGetPosition() (int, int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return f.mainWindow.Pos() +} + +func (f *Frontend) WindowSetSize(width, height int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetSize(width, height) +} + +func (f *Frontend) WindowGetSize() (int, int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return f.mainWindow.Size() +} + +func (f *Frontend) WindowSetTitle(title string) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetText(title) +} + +func (f *Frontend) WindowFullscreen() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = false;") + } + f.mainWindow.Fullscreen() +} + +func (f *Frontend) WindowReloadApp() { + f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL)) +} + +func (f *Frontend) WindowUnfullscreen() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = true;") + } + f.mainWindow.UnFullscreen() +} + +func (f *Frontend) WindowShow() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.ShowWindow() +} + +func (f *Frontend) WindowHide() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.Hide() +} + +func (f *Frontend) WindowMaximise() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.hasStarted { + if !f.frontendOptions.DisableResize { + f.mainWindow.Maximise() + } + } else { + f.frontendOptions.WindowStartState = options.Maximised + } +} + +func (f *Frontend) WindowToggleMaximise() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if !f.hasStarted { + return + } + if f.mainWindow.IsMaximised() { + f.WindowUnmaximise() + } else { + f.WindowMaximise() + } +} + +func (f *Frontend) WindowUnmaximise() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.mainWindow.Form.IsFullScreen() { + return + } + f.mainWindow.Restore() +} + +func (f *Frontend) WindowMinimise() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.hasStarted { + f.mainWindow.Minimise() + } else { + f.frontendOptions.WindowStartState = options.Minimised + } +} + +func (f *Frontend) WindowUnminimise() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.mainWindow.Form.IsFullScreen() { + return + } + f.mainWindow.Restore() +} + +func (f *Frontend) WindowSetMinSize(width int, height int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetMinSize(width, height) +} +func (f *Frontend) WindowSetMaxSize(width int, height int) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + f.mainWindow.SetMaxSize(width, height) +} + +func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) { + if col == nil { + return + } + + f.mainWindow.Invoke(func() { + win32.SetBackgroundColour(f.mainWindow.Handle(), col.R, col.G, col.B) + + controller := f.chromium.GetController() + controller2 := controller.GetICoreWebView2Controller2() + + backgroundCol := edge.COREWEBVIEW2_COLOR{ + A: col.A, + R: col.R, + G: col.G, + B: col.B, + } + + // WebView2 only has 0 and 255 as valid values. + if backgroundCol.A > 0 && backgroundCol.A < 255 { + backgroundCol.A = 255 + } + + if f.frontendOptions.Windows != nil && f.frontendOptions.Windows.WebviewIsTransparent { + backgroundCol.A = 0 + } + + err := controller2.PutDefaultBackgroundColor(backgroundCol) + if err != nil { + log.Fatal(err) + } + }) + +} + +func (f *Frontend) ScreenGetAll() ([]Screen, error) { + var wg sync.WaitGroup + wg.Add(1) + screens := []Screen{} + err := error(nil) + f.mainWindow.Invoke(func() { + screens, err = GetAllScreens(f.mainWindow.Handle()) + wg.Done() + + }) + wg.Wait() + return screens, err +} + +func (f *Frontend) Show() { + f.mainWindow.Show() +} + +func (f *Frontend) Hide() { + f.mainWindow.Hide() +} + +func (f *Frontend) WindowIsMaximised() bool { + return f.mainWindow.IsMaximised() +} + +func (f *Frontend) WindowIsMinimised() bool { + return f.mainWindow.IsMinimised() +} + +func (f *Frontend) WindowIsNormal() bool { + return f.mainWindow.IsNormal() +} + +func (f *Frontend) WindowIsFullscreen() bool { + return f.mainWindow.IsFullScreen() +} + +func (f *Frontend) Quit() { + if f.frontendOptions.OnBeforeClose != nil && f.frontendOptions.OnBeforeClose(f.ctx) { + return + } + // Exit must be called on the Main-Thread. It calls PostQuitMessage which sends the WM_QUIT message to the thread's + // message queue and our message queue runs on the Main-Thread. + f.mainWindow.Invoke(winc.Exit) +} + +func (f *Frontend) WindowPrint() { + f.ExecJS("window.print();") +} + +func (f *Frontend) setupChromium() { + chromium := f.chromium + + disableFeatues := []string{} + if !f.frontendOptions.EnableFraudulentWebsiteDetection { + disableFeatues = append(disableFeatues, "msSmartScreenProtection") + } + + if opts := f.frontendOptions.Windows; opts != nil { + chromium.DataPath = opts.WebviewUserDataPath + chromium.BrowserPath = opts.WebviewBrowserPath + + if opts.WebviewGpuIsDisabled { + chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, "--disable-gpu") + } + if opts.WebviewDisableRendererCodeIntegrity { + disableFeatues = append(disableFeatues, "RendererCodeIntegrity") + } + } + + if len(disableFeatues) > 0 { + arg := fmt.Sprintf("--disable-features=%s", strings.Join(disableFeatues, ",")) + chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, arg) + } + + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.DisableWebViewDrop { + if err := chromium.AllowExternalDrag(false); err != nil { + f.logger.Warning("WebView failed to set AllowExternalDrag to false!") + } + } + + chromium.MessageCallback = f.processMessage + chromium.MessageWithAdditionalObjectsCallback = f.processMessageWithAdditionalObjects + chromium.WebResourceRequestedCallback = f.processRequest + chromium.NavigationCompletedCallback = f.navigationCompleted + chromium.AcceleratorKeyCallback = func(vkey uint) bool { + if vkey == w32.VK_F12 && f.devtoolsEnabled { + var keyState [256]byte + if w32.GetKeyboardState(keyState[:]) { + // Check if CTRL is pressed + if keyState[w32.VK_CONTROL]&0x80 != 0 && keyState[w32.VK_SHIFT]&0x80 != 0 { + chromium.OpenDevToolsWindow() + return true + } + } else { + f.logger.Error("Call to GetKeyboardState failed") + } + } + w32.PostMessage(f.mainWindow.Handle(), w32.WM_KEYDOWN, uintptr(vkey), 0) + return false + } + chromium.ProcessFailedCallback = func(sender *edge.ICoreWebView2, args *edge.ICoreWebView2ProcessFailedEventArgs) { + kind, err := args.GetProcessFailedKind() + if err != nil { + f.logger.Error("GetProcessFailedKind: %s", err) + return + } + + f.logger.Error("WebVie2wProcess failed with kind %d", kind) + switch kind { + case edge.COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED: + // => The app has to recreate a new WebView to recover from this failure. + messages := windows.DefaultMessages() + if f.frontendOptions.Windows != nil && f.frontendOptions.Windows.Messages != nil { + messages = f.frontendOptions.Windows.Messages + } + winc.Errorf(f.mainWindow, messages.WebView2ProcessCrash) + os.Exit(-1) + case edge.COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_EXITED, + edge.COREWEBVIEW2_PROCESS_FAILED_KIND_FRAME_RENDER_PROCESS_EXITED: + // => A new render process is created automatically and navigated to an error page. + // => Make sure that the error page is shown. + if !f.hasStarted { + // NavgiationCompleted didn't come in, make sure the chromium is shown + chromium.Show() + } + if !f.mainWindow.hasBeenShown { + // The window has never been shown, make sure to show it + f.ShowWindow() + } + } + } + + chromium.Embed(f.mainWindow.Handle()) + + if chromium.HasCapability(edge.SwipeNavigation) { + swipeGesturesEnabled := f.frontendOptions.Windows != nil && f.frontendOptions.Windows.EnableSwipeGestures + err := chromium.PutIsSwipeNavigationEnabled(swipeGesturesEnabled) + if err != nil { + log.Fatal(err) + } + } + chromium.Resize() + settings, err := chromium.GetSettings() + if err != nil { + log.Fatal(err) + } + err = settings.PutAreDefaultContextMenusEnabled(f.debug || f.frontendOptions.EnableDefaultContextMenu) + if err != nil { + log.Fatal(err) + } + err = settings.PutAreDevToolsEnabled(f.devtoolsEnabled) + if err != nil { + log.Fatal(err) + } + + if opts := f.frontendOptions.Windows; opts != nil { + if opts.ZoomFactor > 0.0 { + chromium.PutZoomFactor(opts.ZoomFactor) + } + err = settings.PutIsZoomControlEnabled(opts.IsZoomControlEnabled) + if err != nil { + log.Fatal(err) + } + err = settings.PutIsPinchZoomEnabled(!opts.DisablePinchZoom) + if err != nil { + log.Fatal(err) + } + } + + err = settings.PutIsStatusBarEnabled(false) + if err != nil { + log.Fatal(err) + } + err = settings.PutAreBrowserAcceleratorKeysEnabled(false) + if err != nil { + log.Fatal(err) + } + + if f.debug && f.frontendOptions.Debug.OpenInspectorOnStartup { + chromium.OpenDevToolsWindow() + } + + // Setup focus event handler + onFocus := f.mainWindow.OnSetFocus() + onFocus.Bind(f.onFocus) + + // Set background colour + f.WindowSetBackgroundColour(f.frontendOptions.BackgroundColour) + + chromium.SetGlobalPermission(edge.CoreWebView2PermissionStateAllow) + chromium.AddWebResourceRequestedFilter("*", edge.COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL) + chromium.Navigate(f.startURL.String()) +} + +type EventNotify struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (f *Frontend) Notify(name string, data ...interface{}) { + notification := EventNotify{ + Name: name, + Data: data, + } + payload, err := json.Marshal(notification) + if err != nil { + f.logger.Error(err.Error()) + return + } + f.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`) +} + +func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, args *edge.ICoreWebView2WebResourceRequestedEventArgs) { + // Setting the UserAgent on the CoreWebView2Settings clears the whole default UserAgent of the Edge browser, but + // we want to just append our ApplicationIdentifier. So we adjust the UserAgent for every request. + if reqHeaders, err := req.GetHeaders(); err == nil { + useragent, _ := reqHeaders.GetHeader(assetserver.HeaderUserAgent) + useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ") + reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent) + reqHeaders.Release() + } + + if f.assets == nil { + // We are using the devServer let the WebView2 handle the request with its default handler + return + } + + //Get the request + uri, _ := req.GetUri() + reqUri, err := url.ParseRequestURI(uri) + if err != nil { + f.logger.Error("Unable to parse equest uri %s: %s", uri, err) + return + } + + if reqUri.Scheme != f.startURL.Scheme { + // Let the WebView2 handle the request with its default handler + return + } else if reqUri.Host != f.startURL.Host { + // Let the WebView2 handle the request with its default handler + return + } + + webviewRequest, err := webview.NewRequest( + f.chromium.Environment(), + args, + func(fn func()) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if f.mainWindow.InvokeRequired() { + var wg sync.WaitGroup + wg.Add(1) + f.mainWindow.Invoke(func() { + fn() + wg.Done() + }) + wg.Wait() + } else { + fn() + } + }) + + if err != nil { + f.logger.Error("%s: NewRequest failed: %s", uri, err) + return + } + + f.assets.ServeWebViewRequest(webviewRequest) +} + +var edgeMap = map[string]uintptr{ + "n-resize": w32.HTTOP, + "ne-resize": w32.HTTOPRIGHT, + "e-resize": w32.HTRIGHT, + "se-resize": w32.HTBOTTOMRIGHT, + "s-resize": w32.HTBOTTOM, + "sw-resize": w32.HTBOTTOMLEFT, + "w-resize": w32.HTLEFT, + "nw-resize": w32.HTTOPLEFT, +} + +func (f *Frontend) processMessage(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) { + topSource, err := sender.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error())) + return + } + + senderSource, err := args.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error())) + return + } + + // verify both topSource and sender are allowed origins + if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) { + return + } + + if message == "drag" { + if !f.mainWindow.IsFullScreen() { + err := f.startDrag() + if err != nil { + f.logger.Error(err.Error()) + } + } + return + } + + if message == "runtime:ready" { + cmd := fmt.Sprintf( + "window.wails.setCSSDragProperties('%s', '%s');\n"+ + "window.wails.setCSSDropProperties('%s', '%s');", + f.frontendOptions.CSSDragProperty, + f.frontendOptions.CSSDragValue, + f.frontendOptions.DragAndDrop.CSSDropProperty, + f.frontendOptions.DragAndDrop.CSSDropValue, + ) + + f.ExecJS(cmd) + return + } + + if strings.HasPrefix(message, "resize:") { + if !f.mainWindow.IsFullScreen() { + sl := strings.Split(message, ":") + if len(sl) != 2 { + f.logger.Info("Unknown message returned from dispatcher: %+v", message) + return + } + edge := edgeMap[sl[1]] + err := f.startResize(edge) + if err != nil { + f.logger.Error(err.Error()) + } + } + return + } + + go f.dispatchMessage(message) +} + +func (f *Frontend) processMessageWithAdditionalObjects(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) { + topSource, err := sender.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error())) + return + } + + senderSource, err := args.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error())) + return + } + + // verify both topSource and sender are allowed origins + if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) { + return + } + + if strings.HasPrefix(message, "file:drop") { + if !f.frontendOptions.DragAndDrop.EnableFileDrop { + return + } + objs, err := args.GetAdditionalObjects() + if err != nil { + f.logger.Error(err.Error()) + return + } + + defer objs.Release() + + count, err := objs.GetCount() + if err != nil { + f.logger.Error(err.Error()) + return + } + + files := make([]string, count) + for i := uint32(0); i < count; i++ { + _file, err := objs.GetValueAtIndex(i) + if err != nil { + f.logger.Error("cannot get value at %d : %s", i, err.Error()) + return + } + + if _file == nil { + f.logger.Warning("object at %d is not a file", i) + continue + } + + file := (*edge.ICoreWebView2File)(unsafe.Pointer(_file)) + defer file.Release() + + filepath, err := file.GetPath() + if err != nil { + f.logger.Error("cannot get path for object at %d : %s", i, err.Error()) + return + } + + files[i] = filepath + } + + var ( + x = "0" + y = "0" + ) + coords := strings.SplitN(message[10:], ":", 2) + if len(coords) == 2 { + x = coords[0] + y = coords[1] + } + + go f.dispatchMessage(fmt.Sprintf("DD:%s:%s:%s", x, y, strings.Join(files, "\n"))) + return + } +} + +func (f *Frontend) validBindingOrigin(source string) bool { + origin, err := f.originValidator.GetOriginFromURL(source) + if err != nil { + f.logger.Error(fmt.Sprintf("Error parsing source URL %s: %v", source, err.Error())) + return false + } + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + return false + } + return true +} + +func (f *Frontend) dispatchMessage(message string) { + result, err := f.dispatcher.ProcessMessage(message, f) + if err != nil { + f.logger.Error(err.Error()) + f.Callback(result) + return + } + if result == "" { + return + } + + switch result[0] { + case 'c': + // Callback from a method call + f.Callback(result[1:]) + default: + f.logger.Info("Unknown message returned from dispatcher: %+v", result) + } +} + +func (f *Frontend) Callback(message string) { + escaped, err := json.Marshal(message) + if err != nil { + panic(err) + } + f.mainWindow.Invoke(func() { + f.chromium.Eval(`window.wails.Callback(` + string(escaped) + `);`) + }) +} + +func (f *Frontend) startDrag() error { + if !w32.ReleaseCapture() { + return fmt.Errorf("unable to release mouse capture") + } + // Use PostMessage because we don't want to block the caller until dragging has been finished. + w32.PostMessage(f.mainWindow.Handle(), w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0) + return nil +} + +func (f *Frontend) startResize(border uintptr) error { + if !w32.ReleaseCapture() { + return fmt.Errorf("unable to release mouse capture") + } + // Use PostMessage because we don't want to block the caller until resizing has been finished. + w32.PostMessage(f.mainWindow.Handle(), w32.WM_NCLBUTTONDOWN, border, 0) + return nil +} + +func (f *Frontend) ExecJS(js string) { + f.mainWindow.Invoke(func() { + f.chromium.Eval(js) + }) +} + +func (f *Frontend) navigationCompleted(sender *edge.ICoreWebView2, args *edge.ICoreWebView2NavigationCompletedEventArgs) { + if f.frontendOptions.OnDomReady != nil { + go f.frontendOptions.OnDomReady(f.ctx) + } + + if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { + f.ExecJS("window.wails.flags.enableResize = true;") + } + + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + + if f.hasStarted { + return + } + f.hasStarted = true + + // Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026 + err := f.chromium.Hide() + if err != nil { + log.Fatal(err) + } + err = f.chromium.Show() + if err != nil { + log.Fatal(err) + } + + if f.frontendOptions.StartHidden { + return + } + + switch f.frontendOptions.WindowStartState { + case options.Maximised: + if !f.frontendOptions.DisableResize { + win32.ShowWindowMaximised(f.mainWindow.Handle()) + } else { + win32.ShowWindow(f.mainWindow.Handle()) + } + case options.Minimised: + win32.ShowWindowMinimised(f.mainWindow.Handle()) + case options.Fullscreen: + f.mainWindow.Fullscreen() + win32.ShowWindow(f.mainWindow.Handle()) + default: + if f.frontendOptions.Fullscreen { + f.mainWindow.Fullscreen() + } + win32.ShowWindow(f.mainWindow.Handle()) + } + + f.mainWindow.hasBeenShown = true + +} + +func (f *Frontend) ShowWindow() { + f.mainWindow.Invoke(func() { + if !f.mainWindow.hasBeenShown { + f.mainWindow.hasBeenShown = true + switch f.frontendOptions.WindowStartState { + case options.Maximised: + if !f.frontendOptions.DisableResize { + win32.ShowWindowMaximised(f.mainWindow.Handle()) + } else { + win32.ShowWindow(f.mainWindow.Handle()) + } + case options.Minimised: + win32.RestoreWindow(f.mainWindow.Handle()) + case options.Fullscreen: + f.mainWindow.Fullscreen() + win32.ShowWindow(f.mainWindow.Handle()) + default: + if f.frontendOptions.Fullscreen { + f.mainWindow.Fullscreen() + } + win32.ShowWindow(f.mainWindow.Handle()) + } + } else { + if win32.IsWindowMinimised(f.mainWindow.Handle()) { + win32.RestoreWindow(f.mainWindow.Handle()) + } else { + win32.ShowWindow(f.mainWindow.Handle()) + } + } + w32.SetForegroundWindow(f.mainWindow.Handle()) + w32.SetFocus(f.mainWindow.Handle()) + }) + +} + +func (f *Frontend) onFocus(arg *winc.Event) { + f.chromium.Focus() +} + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/windows/keys.go b/v2/internal/frontend/desktop/windows/keys.go new file mode 100644 index 000000000..2fe5f7550 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/keys.go @@ -0,0 +1,203 @@ +//go:build windows +// +build windows + +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/pkg/menu/keys" + "strings" +) + +var ModifierMap = map[keys.Modifier]winc.Modifiers{ + keys.ShiftKey: winc.ModShift, + keys.ControlKey: winc.ModControl, + keys.OptionOrAltKey: winc.ModAlt, + keys.CmdOrCtrlKey: winc.ModControl, +} + +func acceleratorToWincShortcut(accelerator *keys.Accelerator) winc.Shortcut { + + if accelerator == nil { + return winc.NoShortcut + } + inKey := strings.ToUpper(accelerator.Key) + key, exists := keyMap[inKey] + if !exists { + return winc.NoShortcut + } + var modifiers winc.Modifiers + if _, exists := shiftMap[inKey]; exists { + modifiers = winc.ModShift + } + for _, mod := range accelerator.Modifiers { + modifiers |= ModifierMap[mod] + } + return winc.Shortcut{ + Modifiers: modifiers, + Key: key, + } +} + +var shiftMap = map[string]struct{}{ + "~": {}, + ")": {}, + "!": {}, + "@": {}, + "#": {}, + "$": {}, + "%": {}, + "^": {}, + "&": {}, + "*": {}, + "(": {}, + "_": {}, + "PLUS": {}, + "<": {}, + ">": {}, + "?": {}, + ":": {}, + `"`: {}, + "{": {}, + "}": {}, + "|": {}, +} + +var keyMap = map[string]winc.Key{ + "0": winc.Key0, + "1": winc.Key1, + "2": winc.Key2, + "3": winc.Key3, + "4": winc.Key4, + "5": winc.Key5, + "6": winc.Key6, + "7": winc.Key7, + "8": winc.Key8, + "9": winc.Key9, + "A": winc.KeyA, + "B": winc.KeyB, + "C": winc.KeyC, + "D": winc.KeyD, + "E": winc.KeyE, + "F": winc.KeyF, + "G": winc.KeyG, + "H": winc.KeyH, + "I": winc.KeyI, + "J": winc.KeyJ, + "K": winc.KeyK, + "L": winc.KeyL, + "M": winc.KeyM, + "N": winc.KeyN, + "O": winc.KeyO, + "P": winc.KeyP, + "Q": winc.KeyQ, + "R": winc.KeyR, + "S": winc.KeyS, + "T": winc.KeyT, + "U": winc.KeyU, + "V": winc.KeyV, + "W": winc.KeyW, + "X": winc.KeyX, + "Y": winc.KeyY, + "Z": winc.KeyZ, + "F1": winc.KeyF1, + "F2": winc.KeyF2, + "F3": winc.KeyF3, + "F4": winc.KeyF4, + "F5": winc.KeyF5, + "F6": winc.KeyF6, + "F7": winc.KeyF7, + "F8": winc.KeyF8, + "F9": winc.KeyF9, + "F10": winc.KeyF10, + "F11": winc.KeyF11, + "F12": winc.KeyF12, + "F13": winc.KeyF13, + "F14": winc.KeyF14, + "F15": winc.KeyF15, + "F16": winc.KeyF16, + "F17": winc.KeyF17, + "F18": winc.KeyF18, + "F19": winc.KeyF19, + "F20": winc.KeyF20, + "F21": winc.KeyF21, + "F22": winc.KeyF22, + "F23": winc.KeyF23, + "F24": winc.KeyF24, + + "`": winc.KeyOEM3, + ",": winc.KeyOEMComma, + ".": winc.KeyOEMPeriod, + "/": winc.KeyOEM2, + ";": winc.KeyOEM1, + "'": winc.KeyOEM7, + "[": winc.KeyOEM4, + "]": winc.KeyOEM6, + `\`: winc.KeyOEM5, + + "~": winc.KeyOEM3, // + ")": winc.Key0, + "!": winc.Key1, + "@": winc.Key2, + "#": winc.Key3, + "$": winc.Key4, + "%": winc.Key5, + "^": winc.Key6, + "&": winc.Key7, + "*": winc.Key8, + "(": winc.Key9, + "_": winc.KeyOEMMinus, + "PLUS": winc.KeyOEMPlus, + "<": winc.KeyOEMComma, + ">": winc.KeyOEMPeriod, + "?": winc.KeyOEM2, + ":": winc.KeyOEM1, + `"`: winc.KeyOEM7, + "{": winc.KeyOEM4, + "}": winc.KeyOEM6, + "|": winc.KeyOEM5, + + "SPACE": winc.KeySpace, + "TAB": winc.KeyTab, + "CAPSLOCK": winc.KeyCapital, + "NUMLOCK": winc.KeyNumlock, + "SCROLLLOCK": winc.KeyScroll, + "BACKSPACE": winc.KeyBack, + "DELETE": winc.KeyDelete, + "INSERT": winc.KeyInsert, + "RETURN": winc.KeyReturn, + "ENTER": winc.KeyReturn, + "UP": winc.KeyUp, + "DOWN": winc.KeyDown, + "LEFT": winc.KeyLeft, + "RIGHT": winc.KeyRight, + "HOME": winc.KeyHome, + "END": winc.KeyEnd, + "PAGEUP": winc.KeyPrior, + "PAGEDOWN": winc.KeyNext, + "ESCAPE": winc.KeyEscape, + "ESC": winc.KeyEscape, + "VOLUMEUP": winc.KeyVolumeUp, + "VOLUMEDOWN": winc.KeyVolumeDown, + "VOLUMEMUTE": winc.KeyVolumeMute, + "MEDIANEXTTRACK": winc.KeyMediaNextTrack, + "MEDIAPREVIOUSTRACK": winc.KeyMediaPrevTrack, + "MEDIASTOP": winc.KeyMediaStop, + "MEDIAPLAYPAUSE": winc.KeyMediaPlayPause, + "PRINTSCREEN": winc.KeyPrint, + "NUM0": winc.KeyNumpad0, + "NUM1": winc.KeyNumpad1, + "NUM2": winc.KeyNumpad2, + "NUM3": winc.KeyNumpad3, + "NUM4": winc.KeyNumpad4, + "NUM5": winc.KeyNumpad5, + "NUM6": winc.KeyNumpad6, + "NUM7": winc.KeyNumpad7, + "NUM8": winc.KeyNumpad8, + "NUM9": winc.KeyNumpad9, + "nummult": winc.KeyMultiply, + "numadd": winc.KeyAdd, + "numsub": winc.KeySubtract, + "numdec": winc.KeyDecimal, + "numdiv": winc.KeyDivide, +} diff --git a/v2/internal/frontend/desktop/windows/menu.go b/v2/internal/frontend/desktop/windows/menu.go new file mode 100644 index 000000000..b71128b45 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/menu.go @@ -0,0 +1,132 @@ +//go:build windows +// +build windows + +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +var checkboxMap = map[*menu.MenuItem][]*winc.MenuItem{} +var radioGroupMap = map[*menu.MenuItem][]*winc.MenuItem{} + +func toggleCheckBox(menuItem *menu.MenuItem) { + menuItem.Checked = !menuItem.Checked + for _, wincMenu := range checkboxMap[menuItem] { + wincMenu.SetChecked(menuItem.Checked) + } +} + +func addCheckBoxToMap(menuItem *menu.MenuItem, wincMenuItem *winc.MenuItem) { + if checkboxMap[menuItem] == nil { + checkboxMap[menuItem] = []*winc.MenuItem{} + } + checkboxMap[menuItem] = append(checkboxMap[menuItem], wincMenuItem) +} + +func toggleRadioItem(menuItem *menu.MenuItem) { + menuItem.Checked = !menuItem.Checked + for _, wincMenu := range radioGroupMap[menuItem] { + wincMenu.SetChecked(menuItem.Checked) + } +} + +func addRadioItemToMap(menuItem *menu.MenuItem, wincMenuItem *winc.MenuItem) { + if radioGroupMap[menuItem] == nil { + radioGroupMap[menuItem] = []*winc.MenuItem{} + } + radioGroupMap[menuItem] = append(radioGroupMap[menuItem], wincMenuItem) +} + +func (w *Window) SetApplicationMenu(menu *menu.Menu) { + w.applicationMenu = menu + processMenu(w, menu) +} + +func processMenu(window *Window, menu *menu.Menu) { + mainMenu := window.NewMenu() + for _, menuItem := range menu.Items { + submenu := mainMenu.AddSubMenu(menuItem.Label) + if menuItem.SubMenu != nil { + for _, menuItem := range menuItem.SubMenu.Items { + processMenuItem(submenu, menuItem) + } + } + } + mainMenu.Show() +} + +func processMenuItem(parent *winc.MenuItem, menuItem *menu.MenuItem) { + if menuItem.Hidden { + return + } + switch menuItem.Type { + case menu.SeparatorType: + parent.AddSeparator() + case menu.TextType: + shortcut := acceleratorToWincShortcut(menuItem.Accelerator) + newItem := parent.AddItem(menuItem.Label, shortcut) + //if menuItem.Tooltip != "" { + // newItem.SetToolTip(menuItem.Tooltip) + //} + if menuItem.Click != nil { + newItem.OnClick().Bind(func(e *winc.Event) { + menuItem.Click(&menu.CallbackData{ + MenuItem: menuItem, + }) + }) + } + newItem.SetEnabled(!menuItem.Disabled) + + case menu.CheckboxType: + shortcut := acceleratorToWincShortcut(menuItem.Accelerator) + newItem := parent.AddItem(menuItem.Label, shortcut) + newItem.SetCheckable(true) + newItem.SetChecked(menuItem.Checked) + //if menuItem.Tooltip != "" { + // newItem.SetToolTip(menuItem.Tooltip) + //} + if menuItem.Click != nil { + newItem.OnClick().Bind(func(e *winc.Event) { + toggleCheckBox(menuItem) + menuItem.Click(&menu.CallbackData{ + MenuItem: menuItem, + }) + }) + } + newItem.SetEnabled(!menuItem.Disabled) + addCheckBoxToMap(menuItem, newItem) + case menu.RadioType: + shortcut := acceleratorToWincShortcut(menuItem.Accelerator) + newItem := parent.AddItemRadio(menuItem.Label, shortcut) + newItem.SetCheckable(true) + newItem.SetChecked(menuItem.Checked) + //if menuItem.Tooltip != "" { + // newItem.SetToolTip(menuItem.Tooltip) + //} + if menuItem.Click != nil { + newItem.OnClick().Bind(func(e *winc.Event) { + toggleRadioItem(menuItem) + menuItem.Click(&menu.CallbackData{ + MenuItem: menuItem, + }) + }) + } + newItem.SetEnabled(!menuItem.Disabled) + addRadioItemToMap(menuItem, newItem) + case menu.SubmenuType: + submenu := parent.AddSubMenu(menuItem.Label) + for _, menuItem := range menuItem.SubMenu.Items { + processMenuItem(submenu, menuItem) + } + } +} + +func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { + f.mainWindow.SetApplicationMenu(menu) +} + +func (f *Frontend) MenuUpdateApplicationMenu() { + processMenu(f.mainWindow, f.mainWindow.applicationMenu) +} diff --git a/v2/internal/frontend/desktop/windows/screen.go b/v2/internal/frontend/desktop/windows/screen.go new file mode 100644 index 000000000..f6e12bcce --- /dev/null +++ b/v2/internal/frontend/desktop/windows/screen.go @@ -0,0 +1,129 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func MonitorsEqual(first w32.MONITORINFO, second w32.MONITORINFO) bool { + // Checks to make sure all the fields are the same. + // A cleaner way would be to check identity of devices. but I couldn't find a way of doing that using the win32 API + return first.DwFlags == second.DwFlags && + first.RcMonitor.Top == second.RcMonitor.Top && + first.RcMonitor.Bottom == second.RcMonitor.Bottom && + first.RcMonitor.Right == second.RcMonitor.Right && + first.RcMonitor.Left == second.RcMonitor.Left && + first.RcWork.Top == second.RcWork.Top && + first.RcWork.Bottom == second.RcWork.Bottom && + first.RcWork.Right == second.RcWork.Right && + first.RcWork.Left == second.RcWork.Left +} + +func GetMonitorInfo(hMonitor w32.HMONITOR) (*w32.MONITORINFO, error) { + // Adapted from winc.utils.getMonitorInfo TODO: add this to win32 + // See docs for + //https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa + + var info w32.MONITORINFO + info.CbSize = uint32(unsafe.Sizeof(info)) + succeeded := w32.GetMonitorInfo(hMonitor, &info) + if !succeeded { + return &info, errors.New("Windows call to getMonitorInfo failed") + } + return &info, nil +} + +func EnumProc(hMonitor w32.HMONITOR, hdcMonitor w32.HDC, lprcMonitor *w32.RECT, screenContainer *ScreenContainer) uintptr { + // adapted from https://stackoverflow.com/a/23492886/4188138 + + // see docs for the following pages to better understand this function + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow + + ourMonitorData := Screen{} + currentMonHndl := w32.MonitorFromWindow(screenContainer.mainWinHandle, w32.MONITOR_DEFAULTTONEAREST) + currentMonInfo, currErr := GetMonitorInfo(currentMonHndl) + + if currErr != nil { + screenContainer.errors = append(screenContainer.errors, currErr) + screenContainer.monitors = append(screenContainer.monitors, Screen{}) + // not sure what the consequences of returning false are, so let's just return true and handle it ourselves + return w32.TRUE + } + + monInfo, err := GetMonitorInfo(hMonitor) + if err != nil { + screenContainer.errors = append(screenContainer.errors, err) + screenContainer.monitors = append(screenContainer.monitors, Screen{}) + return w32.TRUE + } + + width := lprcMonitor.Right - lprcMonitor.Left + height := lprcMonitor.Bottom - lprcMonitor.Top + ourMonitorData.IsPrimary = monInfo.DwFlags&w32.MONITORINFOF_PRIMARY == 1 + ourMonitorData.Height = int(height) + ourMonitorData.Width = int(width) + ourMonitorData.IsCurrent = MonitorsEqual(*currentMonInfo, *monInfo) + + ourMonitorData.PhysicalSize.Width = int(width) + ourMonitorData.PhysicalSize.Height = int(height) + + var dpiX, dpiY uint + w32.GetDPIForMonitor(hMonitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY) + if dpiX == 0 || dpiY == 0 { + screenContainer.errors = append(screenContainer.errors, fmt.Errorf("unable to get DPI for screen")) + screenContainer.monitors = append(screenContainer.monitors, Screen{}) + return w32.TRUE + } + ourMonitorData.Size.Width = winc.ScaleToDefaultDPI(ourMonitorData.PhysicalSize.Width, dpiX) + ourMonitorData.Size.Height = winc.ScaleToDefaultDPI(ourMonitorData.PhysicalSize.Height, dpiY) + + // the reason we need a container is that we have don't know how many times this function will be called + // this "append" call could potentially do an allocation and rewrite the pointer to monitors. So we save the pointer in screenContainer.monitors + // and retrieve the values after all EnumProc calls + // If EnumProc is multi-threaded, this could be problematic. Although, I don't think it is. + screenContainer.monitors = append(screenContainer.monitors, ourMonitorData) + // let's keep screenContainer.errors the same size as screenContainer.monitors in case we want to match them up later if necessary + screenContainer.errors = append(screenContainer.errors, nil) + return w32.TRUE +} + +type ScreenContainer struct { + monitors []Screen + errors []error + mainWinHandle w32.HWND +} + +func GetAllScreens(mainWinHandle w32.HWND) ([]Screen, error) { + // TODO fix hack of container sharing by having a proper data sharing mechanism between windows and the runtime + monitorContainer := ScreenContainer{mainWinHandle: mainWinHandle} + returnErr := error(nil) + errorStrings := []string{} + + dc := w32.GetDC(0) + defer w32.ReleaseDC(0, dc) + succeeded := w32.EnumDisplayMonitors(dc, nil, syscall.NewCallback(EnumProc), unsafe.Pointer(&monitorContainer)) + if !succeeded { + return monitorContainer.monitors, errors.New("Windows call to EnumDisplayMonitors failed") + } + for idx, err := range monitorContainer.errors { + if err != nil { + errorStrings = append(errorStrings, fmt.Sprintf("Error from monitor #%v, %v", idx+1, err)) + } + } + + if len(errorStrings) > 0 { + returnErr = fmt.Errorf("%v errors encountered: %v", len(errorStrings), errorStrings) + } + return monitorContainer.monitors, returnErr +} diff --git a/v2/internal/frontend/desktop/windows/single_instance.go b/v2/internal/frontend/desktop/windows/single_instance.go new file mode 100644 index 000000000..a02b7edb9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/single_instance.go @@ -0,0 +1,136 @@ +//go:build windows + +package windows + +import ( + "encoding/json" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/pkg/options" + "golang.org/x/sys/windows" + "log" + "os" + "syscall" + "unsafe" +) + +type COPYDATASTRUCT struct { + dwData uintptr + cbData uint32 + lpData uintptr +} + +// WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message +const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 + +func SendMessage(hwnd w32.HWND, data string) { + arrUtf16, _ := syscall.UTF16FromString(data) + + pCopyData := new(COPYDATASTRUCT) + pCopyData.dwData = WMCOPYDATA_SINGLE_INSTANCE_DATA + pCopyData.cbData = uint32(len(arrUtf16)*2 + 1) + pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data))) + + w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) +} + +// SetupSingleInstance single instance Windows app +func SetupSingleInstance(uniqueId string) { + id := "wails-app-" + uniqueId + + className := id + "-sic" + windowName := id + "-siw" + mutexName := id + "sim" + + _, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName)) + + if err != nil { + if err == windows.ERROR_ALREADY_EXISTS { + // app is already running + hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(className), windows.StringToUTF16Ptr(windowName)) + + if hwnd != 0 { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + data.WorkingDirectory, err = os.Getwd() + if err != nil { + log.Printf("Failed to get working directory: %v", err) + return + } + serialized, err := json.Marshal(data) + if err != nil { + log.Printf("Failed to marshal data: %v", err) + return + } + + SendMessage(hwnd, string(serialized)) + // exit second instance of app after sending message + os.Exit(0) + } + // if we got any other unknown error we will just start new application instance + } + } else { + createEventTargetWindow(className, windowName) + } +} + +func createEventTargetWindow(className string, windowName string) w32.HWND { + // callback handler in the event target window + wndProc := func( + hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM, + ) w32.LRESULT { + if msg == w32.WM_COPYDATA { + ldata := (*COPYDATASTRUCT)(unsafe.Pointer(lparam)) + + if ldata.dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { + serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.lpData))) + + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(serialized), &secondInstanceData) + + if err == nil { + secondInstanceBuffer <- secondInstanceData + } + } + + return w32.LRESULT(0) + } + + return w32.DefWindowProc(hwnd, msg, wparam, lparam) + } + + var class w32.WNDCLASSEX + class.Size = uint32(unsafe.Sizeof(class)) + class.Style = 0 + class.WndProc = syscall.NewCallback(wndProc) + class.ClsExtra = 0 + class.WndExtra = 0 + class.Instance = w32.GetModuleHandle("") + class.Icon = 0 + class.Cursor = 0 + class.Background = 0 + class.MenuName = nil + class.ClassName = windows.StringToUTF16Ptr(className) + class.IconSm = 0 + + w32.RegisterClassEx(&class) + + // create event window that will not be visible for user + hwnd := w32.CreateWindowEx( + 0, + windows.StringToUTF16Ptr(className), + windows.StringToUTF16Ptr(windowName), + 0, + 0, + 0, + 0, + 0, + w32.HWND_MESSAGE, + 0, + w32.GetModuleHandle(""), + nil, + ) + + return hwnd +} diff --git a/v2/internal/frontend/desktop/windows/theme.go b/v2/internal/frontend/desktop/windows/theme.go new file mode 100644 index 000000000..ac975e3d0 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/theme.go @@ -0,0 +1,67 @@ +//go:build windows + +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func (w *Window) UpdateTheme() { + + // Don't redraw theme if nothing has changed + if !w.themeChanged { + return + } + w.themeChanged = false + + if win32.IsCurrentlyHighContrastMode() { + return + } + + if !win32.SupportsThemes() { + return + } + + var isDarkMode bool + switch w.theme { + case windows.SystemDefault: + isDarkMode = win32.IsCurrentlyDarkMode() + case windows.Dark: + isDarkMode = true + case windows.Light: + isDarkMode = false + } + win32.SetTheme(w.Handle(), isDarkMode) + + // Custom theme processing + winOptions := w.frontendOptions.Windows + var customTheme *windows.ThemeSettings + if winOptions != nil { + customTheme = winOptions.CustomTheme + } + // Custom theme + if win32.SupportsCustomThemes() && customTheme != nil { + if w.isActive { + if isDarkMode { + win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBar) + win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleText) + win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorder) + } else { + win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBar) + win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleText) + win32.SetBorderColour(w.Handle(), customTheme.LightModeBorder) + } + } else { + if isDarkMode { + win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBarInactive) + win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleTextInactive) + win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorderInactive) + } else { + win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBarInactive) + win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleTextInactive) + win32.SetBorderColour(w.Handle(), customTheme.LightModeBorderInactive) + } + } + } +} diff --git a/v2/internal/frontend/desktop/windows/win32/clipboard.go b/v2/internal/frontend/desktop/windows/win32/clipboard.go new file mode 100644 index 000000000..fa715f18c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/clipboard.go @@ -0,0 +1,143 @@ +//go:build windows + +/* + * Based on code originally from https://github.com/atotto/clipboard. Copyright (c) 2013 Ato Araki. All rights reserved. + */ + +package win32 + +import ( + "runtime" + "syscall" + "time" + "unsafe" +) + +const ( + cfUnicodetext = 13 + gmemMoveable = 0x0002 +) + +// waitOpenClipboard opens the clipboard, waiting for up to a second to do so. +func waitOpenClipboard() error { + started := time.Now() + limit := started.Add(time.Second) + var r uintptr + var err error + for time.Now().Before(limit) { + r, _, err = procOpenClipboard.Call(0) + if r != 0 { + return nil + } + time.Sleep(time.Millisecond) + } + return err +} + +func GetClipboardText() (string, error) { + // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). + // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if formatAvailable, _, err := procIsClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 { + return "", err + } + err := waitOpenClipboard() + if err != nil { + return "", err + } + + h, _, err := procGetClipboardData.Call(cfUnicodetext) + if h == 0 { + _, _, _ = procCloseClipboard.Call() + return "", err + } + + l, _, err := kernelGlobalLock.Call(h) + if l == 0 { + _, _, _ = procCloseClipboard.Call() + return "", err + } + + text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) + + r, _, err := kernelGlobalUnlock.Call(h) + if r == 0 { + _, _, _ = procCloseClipboard.Call() + return "", err + } + + closed, _, err := procCloseClipboard.Call() + if closed == 0 { + return "", err + } + return text, nil +} + +func SetClipboardText(text string) error { + // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). + // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := waitOpenClipboard() + if err != nil { + return err + } + + r, _, err := procEmptyClipboard.Call(0) + if r == 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + + data, err := syscall.UTF16FromString(text) + if err != nil { + return err + } + + // "If the hMem parameter identifies a memory object, the object must have + // been allocated using the function with the GMEM_MOVEABLE flag." + h, _, err := kernelGlobalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) + if h == 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + defer func() { + if h != 0 { + kernelGlobalFree.Call(h) + } + }() + + l, _, err := kernelGlobalLock.Call(h) + if l == 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + + r, _, err = kernelLstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) + if r == 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + + r, _, err = kernelGlobalUnlock.Call(h) + if r == 0 { + if err.(syscall.Errno) != 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + } + + r, _, err = procSetClipboardData.Call(cfUnicodetext, h) + if r == 0 { + _, _, _ = procCloseClipboard.Call() + return err + } + h = 0 // suppress deferred cleanup + closed, _, err := procCloseClipboard.Call() + if closed == 0 { + return err + } + return nil +} diff --git a/v2/internal/frontend/desktop/windows/win32/consts.go b/v2/internal/frontend/desktop/windows/win32/consts.go new file mode 100644 index 000000000..e38ea4b92 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/consts.go @@ -0,0 +1,57 @@ +//go:build windows + +package win32 + +import ( + "syscall" + + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" +) + +type HRESULT int32 +type HANDLE uintptr +type HMONITOR HANDLE + +var ( + moduser32 = syscall.NewLazyDLL("user32.dll") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procGetWindowLong = moduser32.NewProc("GetWindowLongW") + procSetClassLong = moduser32.NewProc("SetClassLongW") + procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") + procShowWindow = moduser32.NewProc("ShowWindow") + procIsWindowVisible = moduser32.NewProc("IsWindowVisible") + procGetWindowRect = moduser32.NewProc("GetWindowRect") + procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW") + procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow") + procIsClipboardFormatAvailable = moduser32.NewProc("IsClipboardFormatAvailable") + procOpenClipboard = moduser32.NewProc("OpenClipboard") + procCloseClipboard = moduser32.NewProc("CloseClipboard") + procEmptyClipboard = moduser32.NewProc("EmptyClipboard") + procGetClipboardData = moduser32.NewProc("GetClipboardData") + procSetClipboardData = moduser32.NewProc("SetClipboardData") +) +var ( + moddwmapi = syscall.NewLazyDLL("dwmapi.dll") + procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") + procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea") +) +var ( + modwingdi = syscall.NewLazyDLL("gdi32.dll") + procCreateSolidBrush = modwingdi.NewProc("CreateSolidBrush") +) +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + kernelGlobalAlloc = kernel32.NewProc("GlobalAlloc") + kernelGlobalFree = kernel32.NewProc("GlobalFree") + kernelGlobalLock = kernel32.NewProc("GlobalLock") + kernelGlobalUnlock = kernel32.NewProc("GlobalUnlock") + kernelLstrcpy = kernel32.NewProc("lstrcpyW") +) + +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return windowsVersion.Major >= major && + windowsVersion.Minor >= minor && + windowsVersion.Build >= buildNumber +} diff --git a/v2/internal/frontend/desktop/windows/win32/theme.go b/v2/internal/frontend/desktop/windows/win32/theme.go new file mode 100644 index 000000000..299a4f63a --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/theme.go @@ -0,0 +1,119 @@ +//go:build windows + +package win32 + +import ( + "unsafe" + + "golang.org/x/sys/windows/registry" +) + +type DWMWINDOWATTRIBUTE int32 + +const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19 +const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20 +const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34 +const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35 +const DwmwaTextColor DWMWINDOWATTRIBUTE = 36 +const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38 + +const SPI_GETHIGHCONTRAST = 0x0042 +const HCF_HIGHCONTRASTON = 0x00000001 + +// BackdropType defines the type of translucency we wish to use +type BackdropType int32 + +func dwmSetWindowAttribute(hwnd uintptr, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) { + ret, _, err := procDwmSetWindowAttribute.Call( + hwnd, + uintptr(dwAttribute), + uintptr(pvAttribute), + cbAttribute) + if ret != 0 { + _ = err + // println(err.Error()) + } +} + +func SupportsThemes() bool { + // We can't support Windows versions before 17763 + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsCustomThemes() bool { + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsBackdropTypes() bool { + return IsWindowsVersionAtLeast(10, 0, 22621) +} + +func SupportsImmersiveDarkMode() bool { + return IsWindowsVersionAtLeast(10, 0, 18985) +} + +func SetTheme(hwnd uintptr, useDarkMode bool) { + if SupportsThemes() { + attr := DwmwaUseImmersiveDarkModeBefore20h1 + if SupportsImmersiveDarkMode() { + attr = DwmwaUseImmersiveDarkMode + } + var winDark int32 + if useDarkMode { + winDark = 1 + } + dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark)) + } +} + +func EnableTranslucency(hwnd uintptr, backdrop BackdropType) { + if SupportsBackdropTypes() { + dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop)) + } else { + println("Warning: Translucency type unavailable on Windows < 22621") + } +} + +func SetTitleBarColour(hwnd uintptr, titleBarColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour)) +} + +func SetTitleTextColour(hwnd uintptr, titleTextColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour)) +} + +func SetBorderColour(hwnd uintptr, titleBorderColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour)) +} + +func IsCurrentlyDarkMode() bool { + key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + + AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme") + if err != nil { + return false + } + return AppsUseLightTheme == 0 +} + +type highContrast struct { + CbSize uint32 + DwFlags uint32 + LpszDefaultScheme *int16 +} + +func IsCurrentlyHighContrastMode() bool { + var result highContrast + result.CbSize = uint32(unsafe.Sizeof(result)) + res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0) + if res == 0 { + _ = err + return false + } + r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON + return r +} diff --git a/v2/internal/frontend/desktop/windows/win32/window.go b/v2/internal/frontend/desktop/windows/win32/window.go new file mode 100644 index 000000000..6028789de --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/window.go @@ -0,0 +1,223 @@ +//go:build windows + +package win32 + +import ( + "fmt" + "log" + "strconv" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" +) + +const ( + WS_MAXIMIZE = 0x01000000 + WS_MINIMIZE = 0x20000000 + + GWL_STYLE = -16 + + MONITOR_DEFAULTTOPRIMARY = 0x00000001 +) + +const ( + SW_HIDE = 0 + SW_NORMAL = 1 + SW_SHOWNORMAL = 1 + SW_SHOWMINIMIZED = 2 + SW_MAXIMIZE = 3 + SW_SHOWMAXIMIZED = 3 + SW_SHOWNOACTIVATE = 4 + SW_SHOW = 5 + SW_MINIMIZE = 6 + SW_SHOWMINNOACTIVE = 7 + SW_SHOWNA = 8 + SW_RESTORE = 9 + SW_SHOWDEFAULT = 10 + SW_FORCEMINIMIZE = 11 +) + +const ( + GCLP_HBRBACKGROUND int32 = -10 +) + +// Power +const ( + // WM_POWERBROADCAST - Notifies applications that a power-management event has occurred. + WM_POWERBROADCAST = 536 + + // PBT_APMPOWERSTATUSCHANGE - Power status has changed. + PBT_APMPOWERSTATUSCHANGE = 10 + + // PBT_APMRESUMEAUTOMATIC -Operation is resuming automatically from a low-power state. This message is sent every time the system resumes. + PBT_APMRESUMEAUTOMATIC = 18 + + // PBT_APMRESUMESUSPEND - Operation is resuming from a low-power state. This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key. + PBT_APMRESUMESUSPEND = 7 + + // PBT_APMSUSPEND - System is suspending operation. + PBT_APMSUSPEND = 4 + + // PBT_POWERSETTINGCHANGE - A power setting change event has been received. + PBT_POWERSETTINGCHANGE = 32787 +) + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb773244.aspx +type MARGINS struct { + CxLeftWidth, CxRightWidth, CyTopHeight, CyBottomHeight int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162897.aspx +type RECT struct { + Left, Top, Right, Bottom int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145065.aspx +type MONITORINFO struct { + CbSize uint32 + RcMonitor RECT + RcWork RECT + DwFlags uint32 +} + +func ExtendFrameIntoClientArea(hwnd uintptr, extend bool) { + // -1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) + // Also shows the caption buttons if transparent ant translucent but they don't work. + // 0: Adds the default frame styling but no aero shadow, does not show the caption buttons. + // 1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) but no caption buttons + // are shown if transparent ant translucent. + var margins MARGINS + if extend { + margins = MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons + } + if err := dwmExtendFrameIntoClientArea(hwnd, &margins); err != nil { + log.Fatal(fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err)) + } +} + +func IsVisible(hwnd uintptr) bool { + ret, _, _ := procIsWindowVisible.Call(hwnd) + return ret != 0 +} + +func IsWindowFullScreen(hwnd uintptr) bool { + wRect := GetWindowRect(hwnd) + m := MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY) + var mi MONITORINFO + mi.CbSize = uint32(unsafe.Sizeof(mi)) + if !GetMonitorInfo(m, &mi) { + return false + } + return wRect.Left == mi.RcMonitor.Left && + wRect.Top == mi.RcMonitor.Top && + wRect.Right == mi.RcMonitor.Right && + wRect.Bottom == mi.RcMonitor.Bottom +} + +func IsWindowMaximised(hwnd uintptr) bool { + style := uint32(getWindowLong(hwnd, GWL_STYLE)) + return style&WS_MAXIMIZE != 0 +} +func IsWindowMinimised(hwnd uintptr) bool { + style := uint32(getWindowLong(hwnd, GWL_STYLE)) + return style&WS_MINIMIZE != 0 +} + +func RestoreWindow(hwnd uintptr) { + showWindow(hwnd, SW_RESTORE) +} + +func ShowWindow(hwnd uintptr) { + showWindow(hwnd, SW_SHOW) +} + +func ShowWindowMaximised(hwnd uintptr) { + showWindow(hwnd, SW_MAXIMIZE) +} +func ShowWindowMinimised(hwnd uintptr) { + showWindow(hwnd, SW_MINIMIZE) +} + +func SetBackgroundColour(hwnd uintptr, r, g, b uint8) { + col := winc.RGB(r, g, b) + hbrush, _, _ := procCreateSolidBrush.Call(uintptr(col)) + setClassLongPtr(hwnd, GCLP_HBRBACKGROUND, hbrush) +} + +func IsWindowNormal(hwnd uintptr) bool { + return !IsWindowMaximised(hwnd) && !IsWindowMinimised(hwnd) && !IsWindowFullScreen(hwnd) +} + +func dwmExtendFrameIntoClientArea(hwnd uintptr, margins *MARGINS) error { + ret, _, _ := procDwmExtendFrameIntoClientArea.Call( + hwnd, + uintptr(unsafe.Pointer(margins))) + + if ret != 0 { + return syscall.GetLastError() + } + + return nil +} + +func setClassLongPtr(hwnd uintptr, param int32, val uintptr) bool { + proc := procSetClassLongPtr + if strconv.IntSize == 32 { + /* + https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclasslongptrw + Note: To write code that is compatible with both 32-bit and 64-bit Windows, use SetClassLongPtr. + When compiling for 32-bit Windows, SetClassLongPtr is defined as a call to the SetClassLong function + + => We have to do this dynamically when directly calling the DLL procedures + */ + proc = procSetClassLong + } + + ret, _, _ := proc.Call( + hwnd, + uintptr(param), + val, + ) + return ret != 0 +} + +func getWindowLong(hwnd uintptr, index int) int32 { + ret, _, _ := procGetWindowLong.Call( + hwnd, + uintptr(index)) + + return int32(ret) +} + +func showWindow(hwnd uintptr, cmdshow int) bool { + ret, _, _ := procShowWindow.Call( + hwnd, + uintptr(cmdshow)) + return ret != 0 +} + +func GetWindowRect(hwnd uintptr) *RECT { + var rect RECT + procGetWindowRect.Call( + hwnd, + uintptr(unsafe.Pointer(&rect))) + + return &rect +} + +func MonitorFromWindow(hwnd uintptr, dwFlags uint32) HMONITOR { + ret, _, _ := procMonitorFromWindow.Call( + hwnd, + uintptr(dwFlags), + ) + return HMONITOR(ret) +} + +func GetMonitorInfo(hMonitor HMONITOR, lmpi *MONITORINFO) bool { + ret, _, _ := procGetMonitorInfo.Call( + uintptr(hMonitor), + uintptr(unsafe.Pointer(lmpi)), + ) + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/.gitignore b/v2/internal/frontend/desktop/windows/winc/.gitignore new file mode 100644 index 000000000..f1c181ec9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/v2/internal/frontend/desktop/windows/winc/AUTHORS b/v2/internal/frontend/desktop/windows/winc/AUTHORS new file mode 100644 index 000000000..85674b8d9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of 'Winc' authors for copyright purposes. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +# Contributors +# ============ + +Tad Vizbaras diff --git a/licenses/github.com/leaanthony/slicer/LICENSE b/v2/internal/frontend/desktop/windows/winc/LICENSE similarity index 97% rename from licenses/github.com/leaanthony/slicer/LICENSE rename to v2/internal/frontend/desktop/windows/winc/LICENSE index 558a6a271..a063a4370 100644 --- a/licenses/github.com/leaanthony/slicer/LICENSE +++ b/v2/internal/frontend/desktop/windows/winc/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Lea Anthony +Copyright (c) 2019 winc Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/v2/internal/frontend/desktop/windows/winc/README.md b/v2/internal/frontend/desktop/windows/winc/README.md new file mode 100644 index 000000000..4d4d467bd --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/README.md @@ -0,0 +1,181 @@ +# winc + +** This is a fork of [tadvi/winc](https://github.com/tadvi/winc) for the sole purpose of integration +with [Wails](https://github.com/wailsapp/wails). This repository comes with ***no support*** ** + +Common library for Go GUI apps on Windows. It is for Windows OS only. This makes library smaller than some other UI +libraries for Go. + +Design goals: minimalism and simplicity. + +## Dependencies + +No other dependencies except Go standard library. + +## Building + +If you want to package icon files and other resources into binary **rsrc** tool is recommended: + + rsrc -manifest app.manifest -ico=app.ico,application_edit.ico,application_error.ico -o rsrc.syso + +Here app.manifest is XML file in format: + +``` + + + + + + + + + +``` + +Most Windows applications do not display command prompt. Build your Go project with flag to indicate that it is Windows +GUI binary: + + go build -ldflags="-H windowsgui" + +## Samples + +Best way to learn how to use the library is to look at the included **examples** projects. + +## Setup + +1. Make sure you have a working Go installation and build environment, see more for details on page below. + http://golang.org/doc/install + +2. go get github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc + +## Icons + +When rsrc is used to pack icons into binary it displays IDs of the packed icons. + +``` +rsrc -manifest app.manifest -ico=app.ico,lightning.ico,edit.ico,application_error.ico -o rsrc.syso +Manifest ID: 1 +Icon app.ico ID: 10 +Icon lightning.ico ID: 13 +Icon edit.ico ID: 16 +Icon application_error.ico ID: 19 +``` + +Use IDs to reference packed icons. + +``` +const myIcon = 13 + +btn.SetResIcon(myIcon) // Set icon on the button. +``` + +Included source **examples** use basic building via `release.bat` files. Note that icon IDs are order dependent. So if +you change they order in -ico flag then icon IDs will be different. If you want to keep order the same, just add new +icons to the end of -ico comma separated list. + +## Layout Manager + +SimpleDock is default layout manager. + +Current design of docking and split views allows building simple apps but if you need to have multiple split views in +few different directions you might need to create your own layout manager. + +Important point is to have **one** control inside SimpleDock set to dock as **Fill**. Controls that are not set to any +docking get placed using SetPos() function. So you can have Panel set to dock at the Top and then have another dock to +arrange controls inside that Panel or have controls placed using SetPos() at fixed positions. + +![Example layout with two toolbars and status bar](dock_topbottom.png) + +This is basic layout. Instead of toolbars and status bar you can have Panel or any other control that can resize. Panel +can have its own internal Dock that will arrange other controls inside of it. + +![Example layout with two toolbars and navigation on the left](dock_topleft.png) + +This is layout with extra control(s) on the left. Left side is usually treeview or listview. + +The rule is simple: you either dock controls using SimpleDock OR use SetPos() to set them at fixed positions. That's it. + +At some point **winc** may get more sophisticated layout manager. + +## Dialog Screens + +Dialog screens are not based on Windows resource files (.rc). They are just windows with controls placed at fixed +coordinates. This works fine for dialog screens up to 10-14 controls. + +# Minimal Demo + +``` +package main + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" +) + +func main() { + mainWindow := winc.NewForm(nil) + mainWindow.SetSize(400, 300) // (width, height) + mainWindow.SetText("Hello World Demo") + + edt := winc.NewEdit(mainWindow) + edt.SetPos(10, 20) + // Most Controls have default size unless SetSize is called. + edt.SetText("edit text") + + btn := winc.NewPushButton(mainWindow) + btn.SetText("Show or Hide") + btn.SetPos(40, 50) // (x, y) + btn.SetSize(100, 40) // (width, height) + btn.OnClick().Bind(func(e *winc.Event) { + if edt.Visible() { + edt.Hide() + } else { + edt.Show() + } + }) + + mainWindow.Center() + mainWindow.Show() + mainWindow.OnClose().Bind(wndOnClose) + + winc.RunMainLoop() // Must call to start event loop. +} + +func wndOnClose(arg *winc.Event) { + winc.Exit() +} +``` + +![Hello World](examples/hello.png) + +Result of running sample_minimal. + +## Create Your Own + +It is good practice to create your own controls based on existing structures and event model. Library contains some of +the controls built that way: IconButton (button.go), ErrorPanel (panel.go), MultiEdit (edit.go), etc. Please look at +existing controls as examples before building your own. + +When designing your own controls keep in mind that types have to be converted from Go into Win32 API and back. This is +usually due to string UTF8 and UTF16 conversions. But there are other types of conversions too. + +When developing your own controls you might also need to: + + import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + +w32 has Win32 API low level constants and functions. + +Look at **sample_control** for example of custom built window. + +## Companion Package + +[Go package for Windows Systray icon, menu and notifications](https://github.com/tadvi/systray) + +## Credits + +This library is built on + +[AllenDang/gform Windows GUI framework for Go](https://github.com/AllenDang/gform) + +**winc** takes most design decisions from **gform** and adds many more controls and code samples to it. + + diff --git a/v2/internal/frontend/desktop/windows/winc/app.go b/v2/internal/frontend/desktop/windows/winc/app.go new file mode 100644 index 000000000..973880444 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/app.go @@ -0,0 +1,109 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ +package winc + +import ( + "runtime" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +var ( + // resource compilation tool assigns app.ico ID of 3 + // rsrc -manifest app.manifest -ico app.ico -o rsrc.syso + AppIconID = 3 +) + +func init() { + runtime.LockOSThread() + + gAppInstance = w32.GetModuleHandle("") + if gAppInstance == 0 { + panic("Error occurred in App.Init") + } + + // Initialize the common controls + var initCtrls w32.INITCOMMONCONTROLSEX + initCtrls.DwSize = uint32(unsafe.Sizeof(initCtrls)) + initCtrls.DwICC = + w32.ICC_LISTVIEW_CLASSES | w32.ICC_PROGRESS_CLASS | w32.ICC_TAB_CLASSES | + w32.ICC_TREEVIEW_CLASSES | w32.ICC_BAR_CLASSES + + w32.InitCommonControlsEx(&initCtrls) +} + +// SetAppIcon sets resource icon ID for the apps windows. +func SetAppIcon(appIconID int) { + AppIconID = appIconID +} + +func GetAppInstance() w32.HINSTANCE { + return gAppInstance +} + +func PreTranslateMessage(msg *w32.MSG) bool { + // This functions is called by the MessageLoop. It processes the + // keyboard accelerator keys and calls Controller.PreTranslateMessage for + // keyboard and mouse events. + + processed := false + + if (msg.Message >= w32.WM_KEYFIRST && msg.Message <= w32.WM_KEYLAST) || + (msg.Message >= w32.WM_MOUSEFIRST && msg.Message <= w32.WM_MOUSELAST) { + + if msg.Hwnd != 0 { + if controller := GetMsgHandler(msg.Hwnd); controller != nil { + // Search the chain of parents for pretranslated messages. + for p := controller; p != nil; p = p.Parent() { + + if processed = p.PreTranslateMessage(msg); processed { + break + } + } + } + } + } + + return processed +} + +// RunMainLoop processes messages in main application loop. +func RunMainLoop() int { + m := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{}))))) + defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m))) + + for w32.GetMessage(m, 0, 0, 0) != 0 { + + if !PreTranslateMessage(m) { + w32.TranslateMessage(m) + w32.DispatchMessage(m) + } + } + + w32.GdiplusShutdown() + return int(m.WParam) +} + +// PostMessages processes recent messages. Sometimes helpful for instant window refresh. +func PostMessages() { + m := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{}))))) + defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m))) + + for i := 0; i < 10; i++ { + if w32.GetMessage(m, 0, 0, 0) != 0 { + if !PreTranslateMessage(m) { + w32.TranslateMessage(m) + w32.DispatchMessage(m) + } + } + } +} + +func Exit() { + w32.PostQuitMessage(0) +} diff --git a/v2/internal/frontend/desktop/windows/winc/bitmap.go b/v2/internal/frontend/desktop/windows/winc/bitmap.go new file mode 100644 index 000000000..c19c31c26 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/bitmap.go @@ -0,0 +1,112 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "errors" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Bitmap struct { + handle w32.HBITMAP + width, height int +} + +func assembleBitmapFromHBITMAP(hbitmap w32.HBITMAP) (*Bitmap, error) { + var dib w32.DIBSECTION + if w32.GetObject(w32.HGDIOBJ(hbitmap), unsafe.Sizeof(dib), unsafe.Pointer(&dib)) == 0 { + return nil, errors.New("GetObject for HBITMAP failed") + } + + return &Bitmap{ + handle: hbitmap, + width: int(dib.DsBmih.BiWidth), + height: int(dib.DsBmih.BiHeight), + }, nil +} + +func NewBitmapFromFile(filepath string, background Color) (*Bitmap, error) { + var gpBitmap *uintptr + var err error + + gpBitmap, err = w32.GdipCreateBitmapFromFile(filepath) + if err != nil { + return nil, err + } + defer w32.GdipDisposeImage(gpBitmap) + + var hbitmap w32.HBITMAP + // Reverse RGB to BGR to satisfy gdiplus color schema. + hbitmap, err = w32.GdipCreateHBITMAPFromBitmap(gpBitmap, uint32(RGB(background.B(), background.G(), background.R()))) + if err != nil { + return nil, err + } + + return assembleBitmapFromHBITMAP(hbitmap) +} + +func NewBitmapFromResource(instance w32.HINSTANCE, resName *uint16, resType *uint16, background Color) (*Bitmap, error) { + var gpBitmap *uintptr + var err error + var hRes w32.HRSRC + + hRes, err = w32.FindResource(w32.HMODULE(instance), resName, resType) + if err != nil { + return nil, err + } + resSize := w32.SizeofResource(w32.HMODULE(instance), hRes) + pResData := w32.LockResource(w32.LoadResource(w32.HMODULE(instance), hRes)) + resBuffer := w32.GlobalAlloc(w32.GMEM_MOVEABLE, resSize) + pResBuffer := w32.GlobalLock(resBuffer) + w32.MoveMemory(pResBuffer, pResData, resSize) + + stream := w32.CreateStreamOnHGlobal(resBuffer, false) + + gpBitmap, err = w32.GdipCreateBitmapFromStream(stream) + if err != nil { + return nil, err + } + defer stream.Release() + defer w32.GlobalUnlock(resBuffer) + defer w32.GlobalFree(resBuffer) + defer w32.GdipDisposeImage(gpBitmap) + + var hbitmap w32.HBITMAP + // Reverse gform.RGB to BGR to satisfy gdiplus color schema. + hbitmap, err = w32.GdipCreateHBITMAPFromBitmap(gpBitmap, uint32(RGB(background.B(), background.G(), background.R()))) + if err != nil { + return nil, err + } + + return assembleBitmapFromHBITMAP(hbitmap) +} + +func (bm *Bitmap) Dispose() { + if bm.handle != 0 { + w32.DeleteObject(w32.HGDIOBJ(bm.handle)) + bm.handle = 0 + } +} + +func (bm *Bitmap) GetHBITMAP() w32.HBITMAP { + return bm.handle +} + +func (bm *Bitmap) Size() (int, int) { + return bm.width, bm.height +} + +func (bm *Bitmap) Height() int { + return bm.height +} + +func (bm *Bitmap) Width() int { + return bm.width +} diff --git a/v2/internal/frontend/desktop/windows/winc/brush.go b/v2/internal/frontend/desktop/windows/winc/brush.go new file mode 100644 index 000000000..1126dd1b9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/brush.go @@ -0,0 +1,74 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +var DefaultBackgroundBrush = NewSystemColorBrush(w32.COLOR_BTNFACE) + +type Brush struct { + hBrush w32.HBRUSH + logBrush w32.LOGBRUSH +} + +func NewSolidColorBrush(color Color) *Brush { + lb := w32.LOGBRUSH{LbStyle: w32.BS_SOLID, LbColor: w32.COLORREF(color)} + hBrush := w32.CreateBrushIndirect(&lb) + if hBrush == 0 { + panic("Faild to create solid color brush") + } + + return &Brush{hBrush, lb} +} + +func NewSystemColorBrush(colorIndex int) *Brush { + //lb := w32.LOGBRUSH{LbStyle: w32.BS_SOLID, LbColor: w32.COLORREF(colorIndex)} + lb := w32.LOGBRUSH{LbStyle: w32.BS_NULL} + hBrush := w32.GetSysColorBrush(colorIndex) + if hBrush == 0 { + panic("GetSysColorBrush failed") + } + return &Brush{hBrush, lb} +} + +func NewHatchedColorBrush(color Color) *Brush { + lb := w32.LOGBRUSH{LbStyle: w32.BS_HATCHED, LbColor: w32.COLORREF(color)} + hBrush := w32.CreateBrushIndirect(&lb) + if hBrush == 0 { + panic("Faild to create solid color brush") + } + + return &Brush{hBrush, lb} +} + +func NewNullBrush() *Brush { + lb := w32.LOGBRUSH{LbStyle: w32.BS_NULL} + hBrush := w32.CreateBrushIndirect(&lb) + if hBrush == 0 { + panic("Failed to create null brush") + } + + return &Brush{hBrush, lb} +} + +func (br *Brush) GetHBRUSH() w32.HBRUSH { + return br.hBrush +} + +func (br *Brush) GetLOGBRUSH() *w32.LOGBRUSH { + return &br.logBrush +} + +func (br *Brush) Dispose() { + if br.hBrush != 0 { + w32.DeleteObject(w32.HGDIOBJ(br.hBrush)) + br.hBrush = 0 + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/buttons.go b/v2/internal/frontend/desktop/windows/winc/buttons.go new file mode 100644 index 000000000..3da801435 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/buttons.go @@ -0,0 +1,156 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Button struct { + ControlBase + onClick EventManager +} + +func (bt *Button) OnClick() *EventManager { + return &bt.onClick +} + +func (bt *Button) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_COMMAND: + bt.onClick.Fire(NewEvent(bt, nil)) + /*case w32.WM_LBUTTONDOWN: + w32.SetCapture(bt.Handle()) + case w32.WM_LBUTTONUP: + w32.ReleaseCapture()*/ + /*case win.WM_GETDLGCODE: + println("GETDLGCODE")*/ + } + return w32.DefWindowProc(bt.hwnd, msg, wparam, lparam) + //return bt.W32Control.WndProc(msg, wparam, lparam) +} + +func (bt *Button) Checked() bool { + result := w32.SendMessage(bt.hwnd, w32.BM_GETCHECK, 0, 0) + return result == w32.BST_CHECKED +} + +func (bt *Button) SetChecked(checked bool) { + wparam := w32.BST_CHECKED + if !checked { + wparam = w32.BST_UNCHECKED + } + w32.SendMessage(bt.hwnd, w32.BM_SETCHECK, uintptr(wparam), 0) +} + +// SetIcon sets icon on the button. Recommended icons are 32x32 with 32bit color depth. +func (bt *Button) SetIcon(ico *Icon) { + w32.SendMessage(bt.hwnd, w32.BM_SETIMAGE, w32.IMAGE_ICON, uintptr(ico.handle)) +} + +func (bt *Button) SetResIcon(iconID uint16) { + if ico, err := NewIconFromResource(GetAppInstance(), iconID); err == nil { + bt.SetIcon(ico) + return + } + panic(fmt.Sprintf("missing icon with icon ID: %d", iconID)) +} + +type PushButton struct { + Button +} + +func NewPushButton(parent Controller) *PushButton { + pb := new(PushButton) + + pb.InitControl("BUTTON", parent, 0, w32.BS_PUSHBUTTON|w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD) + RegMsgHandler(pb) + + pb.SetFont(DefaultFont) + pb.SetText("Button") + pb.SetSize(100, 22) + + return pb +} + +// SetDefault is used for dialogs to set default button. +func (pb *PushButton) SetDefault() { + pb.SetAndClearStyleBits(w32.BS_DEFPUSHBUTTON, w32.BS_PUSHBUTTON) +} + +// IconButton does not display text, requires SetResIcon call. +type IconButton struct { + Button +} + +func NewIconButton(parent Controller) *IconButton { + pb := new(IconButton) + + pb.InitControl("BUTTON", parent, 0, w32.BS_ICON|w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD) + RegMsgHandler(pb) + + pb.SetFont(DefaultFont) + // even if text would be set it would not be displayed + pb.SetText("") + pb.SetSize(100, 22) + + return pb +} + +type CheckBox struct { + Button +} + +func NewCheckBox(parent Controller) *CheckBox { + cb := new(CheckBox) + + cb.InitControl("BUTTON", parent, 0, w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD|w32.BS_AUTOCHECKBOX) + RegMsgHandler(cb) + + cb.SetFont(DefaultFont) + cb.SetText("CheckBox") + cb.SetSize(100, 22) + + return cb +} + +type RadioButton struct { + Button +} + +func NewRadioButton(parent Controller) *RadioButton { + rb := new(RadioButton) + + rb.InitControl("BUTTON", parent, 0, w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD|w32.BS_AUTORADIOBUTTON) + RegMsgHandler(rb) + + rb.SetFont(DefaultFont) + rb.SetText("RadioButton") + rb.SetSize(100, 22) + + return rb +} + +type GroupBox struct { + Button +} + +func NewGroupBox(parent Controller) *GroupBox { + gb := new(GroupBox) + + gb.InitControl("BUTTON", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_GROUP|w32.BS_GROUPBOX) + RegMsgHandler(gb) + + gb.SetFont(DefaultFont) + gb.SetText("GroupBox") + gb.SetSize(100, 100) + + return gb +} diff --git a/v2/internal/frontend/desktop/windows/winc/canvas.go b/v2/internal/frontend/desktop/windows/winc/canvas.go new file mode 100644 index 000000000..0ca4deeb4 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/canvas.go @@ -0,0 +1,159 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Canvas struct { + hwnd w32.HWND + hdc w32.HDC + doNotDispose bool +} + +var nullBrush = NewNullBrush() + +func NewCanvasFromHwnd(hwnd w32.HWND) *Canvas { + hdc := w32.GetDC(hwnd) + if hdc == 0 { + panic(fmt.Sprintf("Create canvas from %v failed.", hwnd)) + } + + return &Canvas{hwnd: hwnd, hdc: hdc, doNotDispose: false} +} + +func NewCanvasFromHDC(hdc w32.HDC) *Canvas { + if hdc == 0 { + panic("Cannot create canvas from invalid HDC.") + } + + return &Canvas{hdc: hdc, doNotDispose: true} +} + +func (ca *Canvas) Dispose() { + if !ca.doNotDispose && ca.hdc != 0 { + if ca.hwnd == 0 { + w32.DeleteDC(ca.hdc) + } else { + w32.ReleaseDC(ca.hwnd, ca.hdc) + } + + ca.hdc = 0 + } +} + +func (ca *Canvas) DrawBitmap(bmp *Bitmap, x, y int) { + cdc := w32.CreateCompatibleDC(0) + defer w32.DeleteDC(cdc) + + hbmpOld := w32.SelectObject(cdc, w32.HGDIOBJ(bmp.GetHBITMAP())) + defer w32.SelectObject(cdc, w32.HGDIOBJ(hbmpOld)) + + w, h := bmp.Size() + + w32.BitBlt(ca.hdc, x, y, w, h, cdc, 0, 0, w32.SRCCOPY) +} + +func (ca *Canvas) DrawStretchedBitmap(bmp *Bitmap, rect *Rect) { + cdc := w32.CreateCompatibleDC(0) + defer w32.DeleteDC(cdc) + + hbmpOld := w32.SelectObject(cdc, w32.HGDIOBJ(bmp.GetHBITMAP())) + defer w32.SelectObject(cdc, w32.HGDIOBJ(hbmpOld)) + + w, h := bmp.Size() + + rc := rect.GetW32Rect() + w32.StretchBlt(ca.hdc, int(rc.Left), int(rc.Top), int(rc.Right), int(rc.Bottom), cdc, 0, 0, w, h, w32.SRCCOPY) +} + +func (ca *Canvas) DrawIcon(ico *Icon, x, y int) bool { + return w32.DrawIcon(ca.hdc, x, y, ico.Handle()) +} + +// DrawFillRect draw and fill rectangle with color. +func (ca *Canvas) DrawFillRect(rect *Rect, pen *Pen, brush *Brush) { + w32Rect := rect.GetW32Rect() + + previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN())) + defer w32.SelectObject(ca.hdc, previousPen) + + previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(brush.GetHBRUSH())) + defer w32.SelectObject(ca.hdc, previousBrush) + + w32.Rectangle(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom) +} + +func (ca *Canvas) DrawRect(rect *Rect, pen *Pen) { + w32Rect := rect.GetW32Rect() + + previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN())) + defer w32.SelectObject(ca.hdc, previousPen) + + // nullBrush is used to make interior of the rect transparent + previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(nullBrush.GetHBRUSH())) + defer w32.SelectObject(ca.hdc, previousBrush) + + w32.Rectangle(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom) +} + +func (ca *Canvas) FillRect(rect *Rect, brush *Brush) { + w32.FillRect(ca.hdc, rect.GetW32Rect(), brush.GetHBRUSH()) +} + +func (ca *Canvas) DrawEllipse(rect *Rect, pen *Pen) { + w32Rect := rect.GetW32Rect() + + previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN())) + defer w32.SelectObject(ca.hdc, previousPen) + + // nullBrush is used to make interior of the rect transparent + previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(nullBrush.GetHBRUSH())) + defer w32.SelectObject(ca.hdc, previousBrush) + + w32.Ellipse(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom) +} + +// DrawFillEllipse draw and fill ellipse with color. +func (ca *Canvas) DrawFillEllipse(rect *Rect, pen *Pen, brush *Brush) { + w32Rect := rect.GetW32Rect() + + previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN())) + defer w32.SelectObject(ca.hdc, previousPen) + + previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(brush.GetHBRUSH())) + defer w32.SelectObject(ca.hdc, previousBrush) + + w32.Ellipse(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom) +} + +func (ca *Canvas) DrawLine(x, y, x2, y2 int, pen *Pen) { + w32.MoveToEx(ca.hdc, x, y, nil) + + previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN())) + defer w32.SelectObject(ca.hdc, previousPen) + + w32.LineTo(ca.hdc, int32(x2), int32(y2)) +} + +// Refer win32 DrawText document for uFormat. +func (ca *Canvas) DrawText(text string, rect *Rect, format uint, font *Font, textColor Color) { + previousFont := w32.SelectObject(ca.hdc, w32.HGDIOBJ(font.GetHFONT())) + defer w32.SelectObject(ca.hdc, w32.HGDIOBJ(previousFont)) + + previousBkMode := w32.SetBkMode(ca.hdc, w32.TRANSPARENT) + defer w32.SetBkMode(ca.hdc, previousBkMode) + + previousTextColor := w32.SetTextColor(ca.hdc, w32.COLORREF(textColor)) + defer w32.SetTextColor(ca.hdc, previousTextColor) + + w32.DrawText(ca.hdc, text, len(text), rect.GetW32Rect(), format) +} diff --git a/v2/internal/frontend/desktop/windows/winc/color.go b/v2/internal/frontend/desktop/windows/winc/color.go new file mode 100644 index 000000000..72b71b865 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/color.go @@ -0,0 +1,26 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +type Color uint32 + +func RGB(r, g, b byte) Color { + return Color(uint32(r) | uint32(g)<<8 | uint32(b)<<16) +} + +func (c Color) R() byte { + return byte(c & 0xff) +} + +func (c Color) G() byte { + return byte((c >> 8) & 0xff) +} + +func (c Color) B() byte { + return byte((c >> 16) & 0xff) +} diff --git a/v2/internal/frontend/desktop/windows/winc/combobox.go b/v2/internal/frontend/desktop/windows/winc/combobox.go new file mode 100644 index 000000000..380ea88d8 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/combobox.go @@ -0,0 +1,70 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ComboBox struct { + ControlBase + onSelectedChange EventManager +} + +func NewComboBox(parent Controller) *ComboBox { + cb := new(ComboBox) + + cb.InitControl("COMBOBOX", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.WS_VSCROLL|w32.CBS_DROPDOWNLIST) + RegMsgHandler(cb) + + cb.SetFont(DefaultFont) + cb.SetSize(200, 400) + return cb +} + +func (cb *ComboBox) DeleteAllItems() bool { + return w32.SendMessage(cb.hwnd, w32.CB_RESETCONTENT, 0, 0) == w32.TRUE +} + +func (cb *ComboBox) InsertItem(index int, str string) bool { + lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str))) + return w32.SendMessage(cb.hwnd, w32.CB_INSERTSTRING, uintptr(index), lp) != w32.CB_ERR +} + +func (cb *ComboBox) DeleteItem(index int) bool { + return w32.SendMessage(cb.hwnd, w32.CB_DELETESTRING, uintptr(index), 0) != w32.CB_ERR +} + +func (cb *ComboBox) SelectedItem() int { + return int(int32(w32.SendMessage(cb.hwnd, w32.CB_GETCURSEL, 0, 0))) +} + +func (cb *ComboBox) SetSelectedItem(value int) bool { + return int(int32(w32.SendMessage(cb.hwnd, w32.CB_SETCURSEL, uintptr(value), 0))) == value +} + +func (cb *ComboBox) OnSelectedChange() *EventManager { + return &cb.onSelectedChange +} + +// Message processor +func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_COMMAND: + code := w32.HIWORD(uint32(wparam)) + + switch code { + case w32.CBN_SELCHANGE: + cb.onSelectedChange.Fire(NewEvent(cb, nil)) + } + } + return w32.DefWindowProc(cb.hwnd, msg, wparam, lparam) + //return cb.W32Control.WndProc(msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/commondlgs.go b/v2/internal/frontend/desktop/windows/winc/commondlgs.go new file mode 100644 index 000000000..07ba47a8f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/commondlgs.go @@ -0,0 +1,125 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func genOFN(parent Controller, title, filter string, filterIndex uint, initialDir string, buf []uint16) *w32.OPENFILENAME { + var ofn w32.OPENFILENAME + ofn.StructSize = uint32(unsafe.Sizeof(ofn)) + ofn.Owner = parent.Handle() + + if filter != "" { + filterBuf := make([]uint16, len(filter)+1) + copy(filterBuf, syscall.StringToUTF16(filter)) + // Replace '|' with the expected '\0' + for i, c := range filterBuf { + if byte(c) == '|' { + filterBuf[i] = uint16(0) + } + } + ofn.Filter = &filterBuf[0] + ofn.FilterIndex = uint32(filterIndex) + } + + ofn.File = &buf[0] + ofn.MaxFile = uint32(len(buf)) + + if initialDir != "" { + ofn.InitialDir = syscall.StringToUTF16Ptr(initialDir) + } + if title != "" { + ofn.Title = syscall.StringToUTF16Ptr(title) + } + + ofn.Flags = w32.OFN_FILEMUSTEXIST + return &ofn +} + +func ShowOpenFileDlg(parent Controller, title, filter string, filterIndex uint, initialDir string) (filePath string, accepted bool) { + buf := make([]uint16, 1024) + ofn := genOFN(parent, title, filter, filterIndex, initialDir, buf) + + if accepted = w32.GetOpenFileName(ofn); accepted { + filePath = syscall.UTF16ToString(buf) + } + return +} + +func ShowSaveFileDlg(parent Controller, title, filter string, filterIndex uint, initialDir string) (filePath string, accepted bool) { + buf := make([]uint16, 1024) + ofn := genOFN(parent, title, filter, filterIndex, initialDir, buf) + + if accepted = w32.GetSaveFileName(ofn); accepted { + filePath = syscall.UTF16ToString(buf) + } + return +} + +func ShowBrowseFolderDlg(parent Controller, title string) (folder string, accepted bool) { + var bi w32.BROWSEINFO + bi.Owner = parent.Handle() + bi.Title = syscall.StringToUTF16Ptr(title) + bi.Flags = w32.BIF_RETURNONLYFSDIRS | w32.BIF_NEWDIALOGSTYLE + + w32.CoInitialize() + ret := w32.SHBrowseForFolder(&bi) + w32.CoUninitialize() + + folder = w32.SHGetPathFromIDList(ret) + accepted = folder != "" + return +} + +// MsgBoxOkCancel basic pop up message. Returns 1 for OK and 2 for CANCEL. +func MsgBoxOkCancel(parent Controller, title, caption string) int { + return MsgBox(parent, title, caption, w32.MB_ICONEXCLAMATION|w32.MB_OKCANCEL) +} + +func MsgBoxYesNo(parent Controller, title, caption string) int { + return MsgBox(parent, title, caption, w32.MB_ICONEXCLAMATION|w32.MB_YESNO) +} + +func MsgBoxOk(parent Controller, title, caption string) { + MsgBox(parent, title, caption, w32.MB_ICONINFORMATION|w32.MB_OK) +} + +// Warningf is generic warning message with OK and Cancel buttons. Returns 1 for OK. +func Warningf(parent Controller, format string, data ...interface{}) int { + caption := fmt.Sprintf(format, data...) + return MsgBox(parent, "Warning", caption, w32.MB_ICONWARNING|w32.MB_OKCANCEL) +} + +// Printf is generic info message with OK button. +func Printf(parent Controller, format string, data ...interface{}) { + caption := fmt.Sprintf(format, data...) + MsgBox(parent, "Information", caption, w32.MB_ICONINFORMATION|w32.MB_OK) +} + +// Errorf is generic error message with OK button. +func Errorf(parent Controller, format string, data ...interface{}) { + caption := fmt.Sprintf(format, data...) + MsgBox(parent, "Error", caption, w32.MB_ICONERROR|w32.MB_OK) +} + +func MsgBox(parent Controller, title, caption string, flags uint) int { + var result int + if parent != nil { + result = w32.MessageBox(parent.Handle(), caption, title, flags) + } else { + result = w32.MessageBox(0, caption, title, flags) + } + + return result +} diff --git a/v2/internal/frontend/desktop/windows/winc/controlbase.go b/v2/internal/frontend/desktop/windows/winc/controlbase.go new file mode 100644 index 000000000..cdb29518c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/controlbase.go @@ -0,0 +1,560 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "runtime" + "sync" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ControlBase struct { + hwnd w32.HWND + font *Font + parent Controller + contextMenu *MenuItem + + isForm bool + + minWidth, minHeight int + maxWidth, maxHeight int + + // General events + onCreate EventManager + onClose EventManager + + // Focus events + onKillFocus EventManager + onSetFocus EventManager + + // Drag and drop events + onDropFiles EventManager + + // Mouse events + onLBDown EventManager + onLBUp EventManager + onLBDbl EventManager + onMBDown EventManager + onMBUp EventManager + onRBDown EventManager + onRBUp EventManager + onRBDbl EventManager + onMouseMove EventManager + + // use MouseControl to capture onMouseHover and onMouseLeave events. + onMouseHover EventManager + onMouseLeave EventManager + + // Keyboard events + onKeyUp EventManager + + // Paint events + onPaint EventManager + onSize EventManager + + m sync.Mutex + dispatchq []func() +} + +// InitControl is called by controls: edit, button, treeview, listview, and so on. +func (cba *ControlBase) InitControl(className string, parent Controller, exstyle, style uint) { + cba.hwnd = CreateWindow(className, parent, exstyle, style) + if cba.hwnd == 0 { + panic("cannot create window for " + className) + } + cba.parent = parent +} + +// InitWindow is called by custom window based controls such as split, panel, etc. +func (cba *ControlBase) InitWindow(className string, parent Controller, exstyle, style uint) { + RegClassOnlyOnce(className) + cba.hwnd = CreateWindow(className, parent, exstyle, style) + if cba.hwnd == 0 { + panic("cannot create window for " + className) + } + cba.parent = parent +} + +// SetTheme for TreeView and ListView controls. +func (cba *ControlBase) SetTheme(appName string) error { + if hr := w32.SetWindowTheme(cba.hwnd, syscall.StringToUTF16Ptr(appName), nil); w32.FAILED(hr) { + return fmt.Errorf("SetWindowTheme %d", hr) + } + return nil +} + +func (cba *ControlBase) Handle() w32.HWND { + return cba.hwnd +} + +func (cba *ControlBase) SetHandle(hwnd w32.HWND) { + cba.hwnd = hwnd +} + +func (cba *ControlBase) GetWindowDPI() (w32.UINT, w32.UINT) { + if w32.HasGetDpiForWindowFunc() { + // GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accureate + // one, especially it is consistent with the WM_DPICHANGED event. + dpi := w32.GetDpiForWindow(cba.hwnd) + return dpi, dpi + } + + if w32.HasGetDPIForMonitorFunc() { + // GetDpiForWindow is supported beginning with Windows 8.1 + monitor := w32.MonitorFromWindow(cba.hwnd, w32.MONITOR_DEFAULTTONEAREST) + if monitor == 0 { + return 0, 0 + } + var dpiX, dpiY w32.UINT + w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY) + return dpiX, dpiY + } + + // If none of the above is supported fallback to the System DPI. + screen := w32.GetDC(0) + x := w32.GetDeviceCaps(screen, w32.LOGPIXELSX) + y := w32.GetDeviceCaps(screen, w32.LOGPIXELSY) + w32.ReleaseDC(0, screen) + return w32.UINT(x), w32.UINT(y) +} + +func (cba *ControlBase) SetAndClearStyleBits(set, clear uint32) error { + style := uint32(w32.GetWindowLong(cba.hwnd, w32.GWL_STYLE)) + if style == 0 { + return fmt.Errorf("GetWindowLong") + } + + if newStyle := style&^clear | set; newStyle != style { + if w32.SetWindowLong(cba.hwnd, w32.GWL_STYLE, newStyle) == 0 { + return fmt.Errorf("SetWindowLong") + } + } + return nil +} + +func (cba *ControlBase) SetIsForm(isform bool) { + cba.isForm = isform +} + +func (cba *ControlBase) SetText(caption string) { + w32.SetWindowText(cba.hwnd, caption) +} + +func (cba *ControlBase) Text() string { + return w32.GetWindowText(cba.hwnd) +} + +func (cba *ControlBase) Close() { + UnRegMsgHandler(cba.hwnd) + w32.DestroyWindow(cba.hwnd) +} + +func (cba *ControlBase) SetTranslucentBackground() { + var accent = w32.ACCENT_POLICY{ + AccentState: w32.ACCENT_ENABLE_BLURBEHIND, + } + var data w32.WINDOWCOMPOSITIONATTRIBDATA + data.Attrib = w32.WCA_ACCENT_POLICY + data.PvData = unsafe.Pointer(&accent) + data.CbData = unsafe.Sizeof(accent) + + w32.SetWindowCompositionAttribute(cba.hwnd, &data) +} + +func (cba *ControlBase) SetContentProtection(enable bool) { + if enable { + w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_EXCLUDEFROMCAPTURE) + } else { + w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_NONE) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func (cba *ControlBase) clampSize(width, height int) (int, int) { + if cba.minWidth != 0 { + width = max(width, cba.minWidth) + } + if cba.maxWidth != 0 { + width = min(width, cba.maxWidth) + } + if cba.minHeight != 0 { + height = max(height, cba.minHeight) + } + if cba.maxHeight != 0 { + height = min(height, cba.maxHeight) + } + return width, height +} + +func (cba *ControlBase) SetSize(width, height int) { + x, y := cba.Pos() + width, height = cba.clampSize(width, height) + width, height = cba.scaleWithWindowDPI(width, height) + w32.MoveWindow(cba.hwnd, x, y, width, height, true) +} + +func (cba *ControlBase) SetMinSize(width, height int) { + cba.minWidth = width + cba.minHeight = height + + // Ensure we set max if min > max + if cba.maxWidth > 0 { + cba.maxWidth = max(cba.minWidth, cba.maxWidth) + } + if cba.maxHeight > 0 { + cba.maxHeight = max(cba.minHeight, cba.maxHeight) + } + + x, y := cba.Pos() + currentWidth, currentHeight := cba.Size() + clampedWidth, clampedHeight := cba.clampSize(currentWidth, currentHeight) + if clampedWidth != currentWidth || clampedHeight != currentHeight { + w32.MoveWindow(cba.hwnd, x, y, clampedWidth, clampedHeight, true) + } +} +func (cba *ControlBase) SetMaxSize(width, height int) { + cba.maxWidth = width + cba.maxHeight = height + + // Ensure we set min if max > min + if cba.maxWidth > 0 { + cba.minWidth = min(cba.maxWidth, cba.minWidth) + } + if cba.maxHeight > 0 { + cba.minHeight = min(cba.maxHeight, cba.minHeight) + } + + x, y := cba.Pos() + currentWidth, currentHeight := cba.Size() + clampedWidth, clampedHeight := cba.clampSize(currentWidth, currentHeight) + if clampedWidth != currentWidth || clampedHeight != currentHeight { + w32.MoveWindow(cba.hwnd, x, y, clampedWidth, clampedHeight, true) + } +} + +func (cba *ControlBase) Size() (width, height int) { + rect := w32.GetWindowRect(cba.hwnd) + width = int(rect.Right - rect.Left) + height = int(rect.Bottom - rect.Top) + width, height = cba.scaleToDefaultDPI(width, height) + return +} + +func (cba *ControlBase) Width() int { + rect := w32.GetWindowRect(cba.hwnd) + return int(rect.Right - rect.Left) +} + +func (cba *ControlBase) Height() int { + rect := w32.GetWindowRect(cba.hwnd) + return int(rect.Bottom - rect.Top) +} + +func (cba *ControlBase) SetPos(x, y int) { + info := getMonitorInfo(cba.hwnd) + workRect := info.RcWork + + w32.SetWindowPos(cba.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE) +} +func (cba *ControlBase) SetAlwaysOnTop(b bool) { + if b { + w32.SetWindowPos(cba.hwnd, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_NOSIZE|w32.SWP_NOMOVE) + } else { + w32.SetWindowPos(cba.hwnd, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_NOSIZE|w32.SWP_NOMOVE) + } +} + +func (cba *ControlBase) Pos() (x, y int) { + rect := w32.GetWindowRect(cba.hwnd) + x = int(rect.Left) + y = int(rect.Top) + if !cba.isForm && cba.parent != nil { + x, y, _ = w32.ScreenToClient(cba.parent.Handle(), x, y) + } + return +} + +func (cba *ControlBase) Visible() bool { + return w32.IsWindowVisible(cba.hwnd) +} + +func (cba *ControlBase) ToggleVisible() bool { + visible := w32.IsWindowVisible(cba.hwnd) + if visible { + cba.Hide() + } else { + cba.Show() + } + return !visible +} + +func (cba *ControlBase) ContextMenu() *MenuItem { + return cba.contextMenu +} + +func (cba *ControlBase) SetContextMenu(menu *MenuItem) { + cba.contextMenu = menu +} + +func (cba *ControlBase) Bounds() *Rect { + rect := w32.GetWindowRect(cba.hwnd) + if cba.isForm { + return &Rect{*rect} + } + + return ScreenToClientRect(cba.hwnd, rect) +} + +func (cba *ControlBase) ClientRect() *Rect { + rect := w32.GetClientRect(cba.hwnd) + return ScreenToClientRect(cba.hwnd, rect) +} +func (cba *ControlBase) ClientWidth() int { + rect := w32.GetClientRect(cba.hwnd) + return int(rect.Right - rect.Left) +} + +func (cba *ControlBase) ClientHeight() int { + rect := w32.GetClientRect(cba.hwnd) + return int(rect.Bottom - rect.Top) +} + +func (cba *ControlBase) Show() { + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + cba.hwnd, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + cba.hwnd, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(cba.hwnd) +} + +func (cba *ControlBase) Hide() { + w32.ShowWindow(cba.hwnd, w32.SW_HIDE) +} + +func (cba *ControlBase) Enabled() bool { + return w32.IsWindowEnabled(cba.hwnd) +} + +func (cba *ControlBase) SetEnabled(b bool) { + w32.EnableWindow(cba.hwnd, b) +} + +func (cba *ControlBase) SetFocus() { + w32.SetFocus(cba.hwnd) +} + +func (cba *ControlBase) Invalidate(erase bool) { + // pRect := w32.GetClientRect(cba.hwnd) + // if cba.isForm { + // w32.InvalidateRect(cba.hwnd, pRect, erase) + // } else { + // rc := ScreenToClientRect(cba.hwnd, pRect) + // w32.InvalidateRect(cba.hwnd, rc.GetW32Rect(), erase) + // } + w32.InvalidateRect(cba.hwnd, nil, erase) +} + +func (cba *ControlBase) Parent() Controller { + return cba.parent +} + +func (cba *ControlBase) SetParent(parent Controller) { + cba.parent = parent +} + +func (cba *ControlBase) Font() *Font { + return cba.font +} + +func (cba *ControlBase) SetFont(font *Font) { + w32.SendMessage(cba.hwnd, w32.WM_SETFONT, uintptr(font.hfont), 1) + cba.font = font +} + +func (cba *ControlBase) EnableDragAcceptFiles(b bool) { + w32.DragAcceptFiles(cba.hwnd, b) +} + +func (cba *ControlBase) InvokeRequired() bool { + if cba.hwnd == 0 { + return false + } + + windowThreadId, _ := w32.GetWindowThreadProcessId(cba.hwnd) + currentThreadId := w32.GetCurrentThreadId() + + return windowThreadId != currentThreadId +} + +func (cba *ControlBase) Invoke(f func()) { + if cba.tryInvokeOnCurrentGoRoutine(f) { + return + } + + cba.m.Lock() + cba.dispatchq = append(cba.dispatchq, f) + cba.m.Unlock() + w32.PostMessage(cba.hwnd, wmInvokeCallback, 0, 0) +} + +func (cba *ControlBase) PreTranslateMessage(msg *w32.MSG) bool { + if msg.Message == w32.WM_GETDLGCODE { + println("pretranslate, WM_GETDLGCODE") + } + return false +} + +// Events +func (cba *ControlBase) OnCreate() *EventManager { + return &cba.onCreate +} + +func (cba *ControlBase) OnClose() *EventManager { + return &cba.onClose +} + +func (cba *ControlBase) OnKillFocus() *EventManager { + return &cba.onKillFocus +} + +func (cba *ControlBase) OnSetFocus() *EventManager { + return &cba.onSetFocus +} + +func (cba *ControlBase) OnDropFiles() *EventManager { + return &cba.onDropFiles +} + +func (cba *ControlBase) OnLBDown() *EventManager { + return &cba.onLBDown +} + +func (cba *ControlBase) OnLBUp() *EventManager { + return &cba.onLBUp +} + +func (cba *ControlBase) OnLBDbl() *EventManager { + return &cba.onLBDbl +} + +func (cba *ControlBase) OnMBDown() *EventManager { + return &cba.onMBDown +} + +func (cba *ControlBase) OnMBUp() *EventManager { + return &cba.onMBUp +} + +func (cba *ControlBase) OnRBDown() *EventManager { + return &cba.onRBDown +} + +func (cba *ControlBase) OnRBUp() *EventManager { + return &cba.onRBUp +} + +func (cba *ControlBase) OnRBDbl() *EventManager { + return &cba.onRBDbl +} + +func (cba *ControlBase) OnMouseMove() *EventManager { + return &cba.onMouseMove +} + +func (cba *ControlBase) OnMouseHover() *EventManager { + return &cba.onMouseHover +} + +func (cba *ControlBase) OnMouseLeave() *EventManager { + return &cba.onMouseLeave +} + +func (cba *ControlBase) OnPaint() *EventManager { + return &cba.onPaint +} + +func (cba *ControlBase) OnSize() *EventManager { + return &cba.onSize +} + +func (cba *ControlBase) OnKeyUp() *EventManager { + return &cba.onKeyUp +} + +func (cba *ControlBase) scaleWithWindowDPI(width, height int) (int, int) { + dpix, dpiy := cba.GetWindowDPI() + scaledWidth := ScaleWithDPI(width, dpix) + scaledHeight := ScaleWithDPI(height, dpiy) + + return scaledWidth, scaledHeight +} + +func (cba *ControlBase) scaleToDefaultDPI(width, height int) (int, int) { + dpix, dpiy := cba.GetWindowDPI() + scaledWidth := ScaleToDefaultDPI(width, dpix) + scaledHeight := ScaleToDefaultDPI(height, dpiy) + + return scaledWidth, scaledHeight +} + +func (cba *ControlBase) tryInvokeOnCurrentGoRoutine(f func()) bool { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if cba.InvokeRequired() { + return false + } + f() + return true +} + +func (cba *ControlBase) invokeCallbacks() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if cba.InvokeRequired() { + panic("InvokeCallbacks must always be called on the window thread") + } + + cba.m.Lock() + q := append([]func(){}, cba.dispatchq...) + cba.dispatchq = []func(){} + cba.m.Unlock() + for _, v := range q { + v() + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/controller.go b/v2/internal/frontend/desktop/windows/winc/controller.go new file mode 100644 index 000000000..b697d9977 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/controller.go @@ -0,0 +1,85 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Controller interface { + Text() string + + Enabled() bool + SetFocus() + + Handle() w32.HWND + Invalidate(erase bool) + Parent() Controller + + Pos() (x, y int) + Size() (w, h int) + Height() int + Width() int + Visible() bool + Bounds() *Rect + ClientRect() *Rect + + SetText(s string) + SetEnabled(b bool) + SetPos(x, y int) + SetSize(w, h int) + EnableDragAcceptFiles(b bool) + Show() + Hide() + + ContextMenu() *MenuItem + SetContextMenu(menu *MenuItem) + + Font() *Font + SetFont(font *Font) + InvokeRequired() bool + Invoke(func()) + PreTranslateMessage(msg *w32.MSG) bool + WndProc(msg uint32, wparam, lparam uintptr) uintptr + + //General events + OnCreate() *EventManager + OnClose() *EventManager + + // Focus events + OnKillFocus() *EventManager + OnSetFocus() *EventManager + + //Drag and drop events + OnDropFiles() *EventManager + + //Mouse events + OnLBDown() *EventManager + OnLBUp() *EventManager + OnLBDbl() *EventManager + OnMBDown() *EventManager + OnMBUp() *EventManager + OnRBDown() *EventManager + OnRBUp() *EventManager + OnRBDbl() *EventManager + OnMouseMove() *EventManager + + // OnMouseLeave and OnMouseHover does not fire unless control called internalTrackMouseEvent. + // Use MouseControl for a how to example. + OnMouseHover() *EventManager + OnMouseLeave() *EventManager + + //Keyboard events + OnKeyUp() *EventManager + + //Paint events + OnPaint() *EventManager + OnSize() *EventManager + + invokeCallbacks() +} diff --git a/v2/internal/frontend/desktop/windows/winc/dialog.go b/v2/internal/frontend/desktop/windows/winc/dialog.go new file mode 100644 index 000000000..6ed87ae4c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/dialog.go @@ -0,0 +1,136 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + +// Dialog displayed as z-order top window until closed. +// It also disables parent window so it can not be clicked. +type Dialog struct { + Form + isModal bool + + btnOk *PushButton + btnCancel *PushButton + + onLoad EventManager + onOk EventManager + onCancel EventManager +} + +func NewDialog(parent Controller) *Dialog { + dlg := new(Dialog) + + dlg.isForm = true + dlg.isModal = true + RegClassOnlyOnce("winc_Dialog") + + dlg.hwnd = CreateWindow("winc_Dialog", parent, w32.WS_EX_CONTROLPARENT, /* IMPORTANT */ + w32.WS_SYSMENU|w32.WS_CAPTION|w32.WS_THICKFRAME /*|w32.WS_BORDER|w32.WS_POPUP*/) + dlg.parent = parent + + // dlg might fail if icon resource is not embedded in the binary + if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil { + dlg.SetIcon(0, ico) + } + + // Dlg forces display of focus rectangles, as soon as the user starts to type. + w32.SendMessage(dlg.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) + RegMsgHandler(dlg) + + dlg.SetFont(DefaultFont) + dlg.SetText("Form") + dlg.SetSize(200, 100) + return dlg +} + +func (dlg *Dialog) SetModal(modal bool) { + dlg.isModal = modal +} + +// SetButtons wires up dialog events to buttons. btnCancel can be nil. +func (dlg *Dialog) SetButtons(btnOk *PushButton, btnCancel *PushButton) { + dlg.btnOk = btnOk + dlg.btnOk.SetDefault() + dlg.btnCancel = btnCancel +} + +// Events +func (dlg *Dialog) OnLoad() *EventManager { + return &dlg.onLoad +} + +func (dlg *Dialog) OnOk() *EventManager { + return &dlg.onOk +} + +func (dlg *Dialog) OnCancel() *EventManager { + return &dlg.onCancel +} + +// PreTranslateMessage handles dialog specific messages. IMPORTANT. +func (dlg *Dialog) PreTranslateMessage(msg *w32.MSG) bool { + if msg.Message >= w32.WM_KEYFIRST && msg.Message <= w32.WM_KEYLAST { + if w32.IsDialogMessage(dlg.hwnd, msg) { + return true + } + } + return false +} + +// Show dialog performs special setup for dialog windows. +func (dlg *Dialog) Show() { + if dlg.isModal { + dlg.Parent().SetEnabled(false) + } + dlg.onLoad.Fire(NewEvent(dlg, nil)) + dlg.Form.Show() +} + +// Close dialog when you done with it. +func (dlg *Dialog) Close() { + if dlg.isModal { + dlg.Parent().SetEnabled(true) + } + dlg.ControlBase.Close() +} + +func (dlg *Dialog) cancel() { + if dlg.btnCancel != nil { + dlg.btnCancel.onClick.Fire(NewEvent(dlg.btnCancel, nil)) + } + dlg.onCancel.Fire(NewEvent(dlg, nil)) +} + +func (dlg *Dialog) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_COMMAND: + switch w32.LOWORD(uint32(wparam)) { + case w32.IDOK: + if dlg.btnOk != nil { + dlg.btnOk.onClick.Fire(NewEvent(dlg.btnOk, nil)) + } + dlg.onOk.Fire(NewEvent(dlg, nil)) + return w32.TRUE + + case w32.IDCANCEL: + dlg.cancel() + return w32.TRUE + } + + case w32.WM_CLOSE: + dlg.cancel() // use onCancel or dlg.btnCancel.OnClick to close + return 0 + + case w32.WM_DESTROY: + if dlg.isModal { + dlg.Parent().SetEnabled(true) + } + } + return w32.DefWindowProc(dlg.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/dock_topbottom.png b/v2/internal/frontend/desktop/windows/winc/dock_topbottom.png new file mode 100644 index 000000000..32ffc2862 Binary files /dev/null and b/v2/internal/frontend/desktop/windows/winc/dock_topbottom.png differ diff --git a/v2/internal/frontend/desktop/windows/winc/dock_topleft.png b/v2/internal/frontend/desktop/windows/winc/dock_topleft.png new file mode 100644 index 000000000..7f466cafb Binary files /dev/null and b/v2/internal/frontend/desktop/windows/winc/dock_topleft.png differ diff --git a/v2/internal/frontend/desktop/windows/winc/edit.go b/v2/internal/frontend/desktop/windows/winc/edit.go new file mode 100644 index 000000000..00e67b71f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/edit.go @@ -0,0 +1,113 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + +type Edit struct { + ControlBase + onChange EventManager +} + +const passwordChar = '*' +const nopasswordChar = ' ' + +func NewEdit(parent Controller) *Edit { + edt := new(Edit) + + edt.InitControl("EDIT", parent, w32.WS_EX_CLIENTEDGE, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.ES_LEFT| + w32.ES_AUTOHSCROLL) + RegMsgHandler(edt) + + edt.SetFont(DefaultFont) + edt.SetSize(200, 22) + return edt +} + +// Events. +func (ed *Edit) OnChange() *EventManager { + return &ed.onChange +} + +// Public methods. +func (ed *Edit) SetReadOnly(isReadOnly bool) { + w32.SendMessage(ed.hwnd, w32.EM_SETREADONLY, uintptr(w32.BoolToBOOL(isReadOnly)), 0) +} + +// Public methods +func (ed *Edit) SetPassword(isPassword bool) { + if isPassword { + w32.SendMessage(ed.hwnd, w32.EM_SETPASSWORDCHAR, uintptr(passwordChar), 0) + } else { + w32.SendMessage(ed.hwnd, w32.EM_SETPASSWORDCHAR, 0, 0) + } +} + +func (ed *Edit) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_COMMAND: + switch w32.HIWORD(uint32(wparam)) { + case w32.EN_CHANGE: + ed.onChange.Fire(NewEvent(ed, nil)) + } + /*case w32.WM_GETDLGCODE: + println("Edit") + if wparam == w32.VK_RETURN { + return w32.DLGC_WANTALLKEYS + }*/ + } + return w32.DefWindowProc(ed.hwnd, msg, wparam, lparam) +} + +// MultiEdit is multiline text edit. +type MultiEdit struct { + ControlBase + onChange EventManager +} + +func NewMultiEdit(parent Controller) *MultiEdit { + med := new(MultiEdit) + + med.InitControl("EDIT", parent, w32.WS_EX_CLIENTEDGE, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.ES_LEFT| + w32.WS_VSCROLL|w32.WS_HSCROLL|w32.ES_MULTILINE|w32.ES_WANTRETURN|w32.ES_AUTOHSCROLL|w32.ES_AUTOVSCROLL) + RegMsgHandler(med) + + med.SetFont(DefaultFont) + med.SetSize(200, 400) + return med +} + +// Events +func (med *MultiEdit) OnChange() *EventManager { + return &med.onChange +} + +// Public methods +func (med *MultiEdit) SetReadOnly(isReadOnly bool) { + w32.SendMessage(med.hwnd, w32.EM_SETREADONLY, uintptr(w32.BoolToBOOL(isReadOnly)), 0) +} + +func (med *MultiEdit) AddLine(text string) { + if len(med.Text()) == 0 { + med.SetText(text) + } else { + med.SetText(med.Text() + "\r\n" + text) + } +} + +func (med *MultiEdit) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + + case w32.WM_COMMAND: + switch w32.HIWORD(uint32(wparam)) { + case w32.EN_CHANGE: + med.onChange.Fire(NewEvent(med, nil)) + } + } + return w32.DefWindowProc(med.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/event.go b/v2/internal/frontend/desktop/windows/winc/event.go new file mode 100644 index 000000000..12f894f60 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/event.go @@ -0,0 +1,17 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +type Event struct { + Sender Controller + Data interface{} +} + +func NewEvent(sender Controller, data interface{}) *Event { + return &Event{Sender: sender, Data: data} +} diff --git a/v2/internal/frontend/desktop/windows/winc/eventdata.go b/v2/internal/frontend/desktop/windows/winc/eventdata.go new file mode 100644 index 000000000..32798ebf4 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/eventdata.go @@ -0,0 +1,52 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type RawMsg struct { + Hwnd w32.HWND + Msg uint32 + WParam, LParam uintptr +} + +type MouseEventData struct { + X, Y int + Button int + Wheel int +} + +type DropFilesEventData struct { + X, Y int + Files []string +} + +type PaintEventData struct { + Canvas *Canvas +} + +type LabelEditEventData struct { + Item ListItem + Text string + //PszText *uint16 +} + +/*type LVDBLClickEventData struct { + NmItem *w32.NMITEMACTIVATE +}*/ + +type KeyUpEventData struct { + VKey, Code int +} + +type SizeEventData struct { + Type uint + X, Y int +} diff --git a/v2/internal/frontend/desktop/windows/winc/eventmanager.go b/v2/internal/frontend/desktop/windows/winc/eventmanager.go new file mode 100644 index 000000000..f4372e9c1 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/eventmanager.go @@ -0,0 +1,24 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +type EventHandler func(arg *Event) + +type EventManager struct { + handler EventHandler +} + +func (evm *EventManager) Fire(arg *Event) { + if evm.handler != nil { + evm.handler(arg) + } +} + +func (evm *EventManager) Bind(handler EventHandler) { + evm.handler = handler +} diff --git a/v2/internal/frontend/desktop/windows/winc/font.go b/v2/internal/frontend/desktop/windows/winc/font.go new file mode 100644 index 000000000..314f1bbdf --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/font.go @@ -0,0 +1,121 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +const ( + FontBold byte = 0x01 + FontItalic byte = 0x02 + FontUnderline byte = 0x04 + FontStrikeOut byte = 0x08 +) + +func init() { + DefaultFont = NewFont("MS Shell Dlg 2", 8, 0) +} + +type Font struct { + hfont w32.HFONT + family string + pointSize int + style byte +} + +func NewFont(family string, pointSize int, style byte) *Font { + if style > FontBold|FontItalic|FontUnderline|FontStrikeOut { + panic("Invalid font style") + } + + //Retrive screen DPI + hDC := w32.GetDC(0) + defer w32.ReleaseDC(0, hDC) + screenDPIY := w32.GetDeviceCaps(hDC, w32.LOGPIXELSY) + + font := Font{ + family: family, + pointSize: pointSize, + style: style, + } + + font.hfont = font.createForDPI(screenDPIY) + if font.hfont == 0 { + panic("CreateFontIndirect failed") + } + + return &font +} + +func (fnt *Font) createForDPI(dpi int) w32.HFONT { + var lf w32.LOGFONT + + lf.Height = int32(-w32.MulDiv(fnt.pointSize, dpi, 72)) + if fnt.style&FontBold > 0 { + lf.Weight = w32.FW_BOLD + } else { + lf.Weight = w32.FW_NORMAL + } + if fnt.style&FontItalic > 0 { + lf.Italic = 1 + } + if fnt.style&FontUnderline > 0 { + lf.Underline = 1 + } + if fnt.style&FontStrikeOut > 0 { + lf.StrikeOut = 1 + } + lf.CharSet = w32.DEFAULT_CHARSET + lf.OutPrecision = w32.OUT_TT_PRECIS + lf.ClipPrecision = w32.CLIP_DEFAULT_PRECIS + lf.Quality = w32.CLEARTYPE_QUALITY + lf.PitchAndFamily = w32.VARIABLE_PITCH | w32.FF_SWISS + + src := syscall.StringToUTF16(fnt.family) + dest := lf.FaceName[:] + copy(dest, src) + + return w32.CreateFontIndirect(&lf) +} + +func (fnt *Font) GetHFONT() w32.HFONT { + return fnt.hfont +} + +func (fnt *Font) Bold() bool { + return fnt.style&FontBold > 0 +} + +func (fnt *Font) Dispose() { + if fnt.hfont != 0 { + w32.DeleteObject(w32.HGDIOBJ(fnt.hfont)) + } +} + +func (fnt *Font) Family() string { + return fnt.family +} + +func (fnt *Font) Italic() bool { + return fnt.style&FontItalic > 0 +} + +func (fnt *Font) StrikeOut() bool { + return fnt.style&FontStrikeOut > 0 +} + +func (fnt *Font) Underline() bool { + return fnt.style&FontUnderline > 0 +} + +func (fnt *Font) Style() byte { + return fnt.style +} diff --git a/v2/internal/frontend/desktop/windows/winc/form.go b/v2/internal/frontend/desktop/windows/winc/form.go new file mode 100644 index 000000000..c9acf7278 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/form.go @@ -0,0 +1,317 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type LayoutManager interface { + Update() +} + +// A Form is main window of the application. +type Form struct { + ControlBase + + layoutMng LayoutManager + + // Fullscreen / Unfullscreen + isFullscreen bool + previousWindowStyle uint32 + previousWindowExStyle uint32 + previousWindowPlacement w32.WINDOWPLACEMENT +} + +func NewCustomForm(parent Controller, exStyle int, dwStyle uint) *Form { + fm := new(Form) + + RegClassOnlyOnce("winc_Form") + + fm.isForm = true + + if exStyle == 0 { + exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW + } + + if dwStyle == 0 { + dwStyle = w32.WS_OVERLAPPEDWINDOW + } + + fm.hwnd = CreateWindow("winc_Form", parent, uint(exStyle), dwStyle) + fm.parent = parent + + // this might fail if icon resource is not embedded in the binary + if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil { + fm.SetIcon(0, ico) + } + + // This forces display of focus rectangles, as soon as the user starts to type. + w32.SendMessage(fm.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) + + RegMsgHandler(fm) + + fm.SetFont(DefaultFont) + fm.SetText("Form") + return fm +} + +func NewForm(parent Controller) *Form { + fm := new(Form) + + RegClassOnlyOnce("winc_Form") + + fm.isForm = true + fm.hwnd = CreateWindow("winc_Form", parent, w32.WS_EX_CONTROLPARENT|w32.WS_EX_APPWINDOW, w32.WS_OVERLAPPEDWINDOW) + fm.parent = parent + + // this might fail if icon resource is not embedded in the binary + if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil { + fm.SetIcon(0, ico) + } + + // This forces display of focus rectangles, as soon as the user starts to type. + w32.SendMessage(fm.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) + + RegMsgHandler(fm) + + fm.SetFont(DefaultFont) + fm.SetText("Form") + return fm +} + +func (fm *Form) SetLayout(mng LayoutManager) { + fm.layoutMng = mng +} + +// UpdateLayout refresh layout. +func (fm *Form) UpdateLayout() { + if fm.layoutMng != nil { + fm.layoutMng.Update() + } +} + +func (fm *Form) NewMenu() *Menu { + hMenu := w32.CreateMenu() + if hMenu == 0 { + panic("failed CreateMenu") + } + m := &Menu{hMenu: hMenu, hwnd: fm.hwnd} + if !w32.SetMenu(fm.hwnd, hMenu) { + panic("failed SetMenu") + } + return m +} + +func (fm *Form) DisableIcon() { + windowInfo := getWindowInfo(fm.hwnd) + frameless := windowInfo.IsPopup() + if frameless { + return + } + exStyle := w32.GetWindowLong(fm.hwnd, w32.GWL_EXSTYLE) + w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, uint32(exStyle|w32.WS_EX_DLGMODALFRAME)) + w32.SetWindowPos(fm.hwnd, 0, 0, 0, 0, 0, + uint( + w32.SWP_FRAMECHANGED| + w32.SWP_NOMOVE| + w32.SWP_NOSIZE| + w32.SWP_NOZORDER), + ) +} + +func (fm *Form) Maximise() { + w32.ShowWindow(fm.hwnd, w32.SW_MAXIMIZE) +} + +func (fm *Form) Minimise() { + w32.ShowWindow(fm.hwnd, w32.SW_MINIMIZE) +} + +func (fm *Form) Restore() { + // SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized + const SC_RESTORE = 0xF120 + // restore the minimized window, if it is + w32.SendMessage( + fm.hwnd, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) + w32.ShowWindow(fm.hwnd, w32.SW_SHOW) +} + +// Public methods +func (fm *Form) Center() { + + windowInfo := getWindowInfo(fm.hwnd) + frameless := windowInfo.IsPopup() + + info := getMonitorInfo(fm.hwnd) + workRect := info.RcWork + screenMiddleW := workRect.Left + (workRect.Right-workRect.Left)/2 + screenMiddleH := workRect.Top + (workRect.Bottom-workRect.Top)/2 + var winRect *w32.RECT + if !frameless { + winRect = w32.GetWindowRect(fm.hwnd) + } else { + winRect = w32.GetClientRect(fm.hwnd) + } + winWidth := winRect.Right - winRect.Left + winHeight := winRect.Bottom - winRect.Top + windowX := screenMiddleW - (winWidth / 2) + windowY := screenMiddleH - (winHeight / 2) + w32.SetWindowPos(fm.hwnd, w32.HWND_TOP, int(windowX), int(windowY), int(winWidth), int(winHeight), w32.SWP_NOSIZE) +} + +func (fm *Form) Fullscreen() { + if fm.isFullscreen { + return + } + + fm.previousWindowStyle = uint32(w32.GetWindowLongPtr(fm.hwnd, w32.GWL_STYLE)) + fm.previousWindowExStyle = uint32(w32.GetWindowLong(fm.hwnd, w32.GWL_EXSTYLE)) + + monitor := w32.MonitorFromWindow(fm.hwnd, w32.MONITOR_DEFAULTTOPRIMARY) + var monitorInfo w32.MONITORINFO + monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) + if !w32.GetMonitorInfo(monitor, &monitorInfo) { + return + } + if !w32.GetWindowPlacement(fm.hwnd, &fm.previousWindowPlacement) { + return + } + // According to https://devblogs.microsoft.com/oldnewthing/20050505-04/?p=35703 one should use w32.WS_POPUP | w32.WS_VISIBLE + w32.SetWindowLong(fm.hwnd, w32.GWL_STYLE, fm.previousWindowStyle & ^uint32(w32.WS_OVERLAPPEDWINDOW) | (w32.WS_POPUP|w32.WS_VISIBLE)) + w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, fm.previousWindowExStyle & ^uint32(w32.WS_EX_DLGMODALFRAME)) + fm.isFullscreen = true + w32.SetWindowPos(fm.hwnd, w32.HWND_TOP, + int(monitorInfo.RcMonitor.Left), + int(monitorInfo.RcMonitor.Top), + int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left), + int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top), + w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED) +} + +func (fm *Form) UnFullscreen() { + if !fm.isFullscreen { + return + } + w32.SetWindowLong(fm.hwnd, w32.GWL_STYLE, fm.previousWindowStyle) + w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, fm.previousWindowExStyle) + w32.SetWindowPlacement(fm.hwnd, &fm.previousWindowPlacement) + fm.isFullscreen = false + w32.SetWindowPos(fm.hwnd, 0, 0, 0, 0, 0, + w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED) +} + +func (fm *Form) IsFullScreen() bool { + return fm.isFullscreen +} + +// IconType: 1 - ICON_BIG; 0 - ICON_SMALL +func (fm *Form) SetIcon(iconType int, icon *Icon) { + if iconType > 1 { + panic("IconType is invalid") + } + w32.SendMessage(fm.hwnd, w32.WM_SETICON, uintptr(iconType), uintptr(icon.Handle())) +} + +func (fm *Form) EnableMaxButton(b bool) { + SetStyle(fm.hwnd, b, w32.WS_MAXIMIZEBOX) +} + +func (fm *Form) EnableMinButton(b bool) { + SetStyle(fm.hwnd, b, w32.WS_MINIMIZEBOX) +} + +func (fm *Form) EnableSizable(b bool) { + SetStyle(fm.hwnd, b, w32.WS_THICKFRAME) +} + +func (fm *Form) EnableDragMove(_ bool) { + //fm.isDragMove = b +} + +func (fm *Form) EnableTopMost(b bool) { + tag := w32.HWND_NOTOPMOST + if b { + tag = w32.HWND_TOPMOST + } + w32.SetWindowPos(fm.hwnd, tag, 0, 0, 0, 0, w32.SWP_NOMOVE|w32.SWP_NOSIZE) +} + +func (fm *Form) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + + switch msg { + case w32.WM_COMMAND: + if lparam == 0 && w32.HIWORD(uint32(wparam)) == 0 { + // Menu support. + actionID := uint16(w32.LOWORD(uint32(wparam))) + if action, ok := actionsByID[actionID]; ok { + action.onClick.Fire(NewEvent(fm, nil)) + } + } + case w32.WM_KEYDOWN: + // Accelerator support. + key := Key(wparam) + if uint32(lparam)>>30 == 0 { + // Using TranslateAccelerators refused to work, so we handle them + // ourselves, at least for now. + shortcut := Shortcut{ModifiersDown(), key} + if action, ok := shortcut2Action[shortcut]; ok { + if action.Enabled() { + action.onClick.Fire(NewEvent(fm, nil)) + } + } + } + + case w32.WM_CLOSE: + return 0 + case w32.WM_DESTROY: + w32.PostQuitMessage(0) + return 0 + + case w32.WM_SIZE, w32.WM_PAINT: + if fm.layoutMng != nil { + fm.layoutMng.Update() + } + case w32.WM_GETMINMAXINFO: + mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam)) + hasConstraints := false + if fm.minWidth > 0 || fm.minHeight > 0 { + hasConstraints = true + + width, height := fm.scaleWithWindowDPI(fm.minWidth, fm.minHeight) + if width > 0 { + mmi.PtMinTrackSize.X = int32(width) + } + if height > 0 { + mmi.PtMinTrackSize.Y = int32(height) + } + } + if fm.maxWidth > 0 || fm.maxHeight > 0 { + hasConstraints = true + + width, height := fm.scaleWithWindowDPI(fm.maxWidth, fm.maxHeight) + if width > 0 { + mmi.PtMaxTrackSize.X = int32(width) + } + if height > 0 { + mmi.PtMaxTrackSize.Y = int32(height) + } + } + if hasConstraints { + return 0 + } + } + + return w32.DefWindowProc(fm.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/globalvars.go b/v2/internal/frontend/desktop/windows/winc/globalvars.go new file mode 100644 index 000000000..46777da0f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/globalvars.go @@ -0,0 +1,27 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// Private global variables. +var ( + gAppInstance w32.HINSTANCE + gControllerRegistry map[w32.HWND]Controller + gRegisteredClasses []string +) + +// Public global variables. +var ( + GeneralWndprocCallBack = syscall.NewCallback(generalWndProc) + DefaultFont *Font +) diff --git a/v2/internal/frontend/desktop/windows/winc/icon.go b/v2/internal/frontend/desktop/windows/winc/icon.go new file mode 100644 index 000000000..6a3e1a391 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/icon.go @@ -0,0 +1,55 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "errors" + "fmt" + "syscall" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Icon struct { + handle w32.HICON +} + +func NewIconFromFile(path string) (*Icon, error) { + ico := new(Icon) + var err error + if ico.handle = w32.LoadIcon(0, syscall.StringToUTF16Ptr(path)); ico.handle == 0 { + err = errors.New(fmt.Sprintf("Cannot load icon from %s", path)) + } + return ico, err +} + +func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (*Icon, error) { + ico := new(Icon) + var err error + if ico.handle = w32.LoadIconWithResourceID(instance, resId); ico.handle == 0 { + err = errors.New(fmt.Sprintf("Cannot load icon from resource with id %v", resId)) + } + return ico, err +} + +func ExtractIcon(fileName string, index int) (*Icon, error) { + ico := new(Icon) + var err error + if ico.handle = w32.ExtractIcon(fileName, index); ico.handle == 0 || ico.handle == 1 { + err = errors.New(fmt.Sprintf("Cannot extract icon from %s at index %v", fileName, index)) + } + return ico, err +} + +func (ic *Icon) Destroy() bool { + return w32.DestroyIcon(ic.handle) +} + +func (ic *Icon) Handle() w32.HICON { + return ic.handle +} diff --git a/v2/internal/frontend/desktop/windows/winc/imagelist.go b/v2/internal/frontend/desktop/windows/winc/imagelist.go new file mode 100644 index 000000000..c540a816d --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/imagelist.go @@ -0,0 +1,64 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ImageList struct { + handle w32.HIMAGELIST +} + +func NewImageList(cx, cy int) *ImageList { + return newImageList(cx, cy, w32.ILC_COLOR32, 0, 0) +} + +func newImageList(cx, cy int, flags uint, cInitial, cGrow int) *ImageList { + imgl := new(ImageList) + imgl.handle = w32.ImageList_Create(cx, cy, flags, cInitial, cGrow) + return imgl +} + +func (im *ImageList) Handle() w32.HIMAGELIST { + return im.handle +} + +func (im *ImageList) Destroy() bool { + return w32.ImageList_Destroy(im.handle) +} + +func (im *ImageList) SetImageCount(uNewCount uint) bool { + return w32.ImageList_SetImageCount(im.handle, uNewCount) +} + +func (im *ImageList) ImageCount() int { + return w32.ImageList_GetImageCount(im.handle) +} + +func (im *ImageList) AddIcon(icon *Icon) int { + return w32.ImageList_AddIcon(im.handle, icon.Handle()) +} + +func (im *ImageList) AddResIcon(iconID uint16) { + if ico, err := NewIconFromResource(GetAppInstance(), iconID); err == nil { + im.AddIcon(ico) + return + } + panic(fmt.Sprintf("missing icon with icon ID: %d", iconID)) +} + +func (im *ImageList) RemoveAll() bool { + return w32.ImageList_RemoveAll(im.handle) +} + +func (im *ImageList) Remove(i int) bool { + return w32.ImageList_Remove(im.handle, i) +} diff --git a/v2/internal/frontend/desktop/windows/winc/imageview.go b/v2/internal/frontend/desktop/windows/winc/imageview.go new file mode 100644 index 000000000..8e3ae50b3 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/imageview.go @@ -0,0 +1,59 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + +type ImageView struct { + ControlBase + + bmp *Bitmap +} + +func NewImageView(parent Controller) *ImageView { + iv := new(ImageView) + + iv.InitWindow("winc_ImageView", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + RegMsgHandler(iv) + + iv.SetFont(DefaultFont) + iv.SetText("") + iv.SetSize(200, 65) + return iv +} + +func (iv *ImageView) DrawImageFile(filepath string) error { + bmp, err := NewBitmapFromFile(filepath, RGB(255, 255, 0)) + if err != nil { + return err + } + iv.bmp = bmp + return nil +} + +func (iv *ImageView) DrawImage(bmp *Bitmap) { + iv.bmp = bmp +} + +func (iv *ImageView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_SIZE, w32.WM_SIZING: + iv.Invalidate(true) + + case w32.WM_ERASEBKGND: + return 1 // important + + case w32.WM_PAINT: + if iv.bmp != nil { + canvas := NewCanvasFromHwnd(iv.hwnd) + defer canvas.Dispose() + iv.SetSize(iv.bmp.Size()) + canvas.DrawBitmap(iv.bmp, 0, 0) + } + } + return w32.DefWindowProc(iv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/imageviewbox.go b/v2/internal/frontend/desktop/windows/winc/imageviewbox.go new file mode 100644 index 000000000..0f6f57be9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/imageviewbox.go @@ -0,0 +1,342 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "time" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type direction int + +const ( + DirNone direction = iota + DirX + DirY + DirX2 + DirY2 +) + +var ImageBoxPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(140, 140, 220))) +var ImageBoxHiPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(220, 140, 140))) +var ImageBoxMarkBrush = NewSolidColorBrush(RGB(40, 40, 40)) +var ImageBoxMarkPen = NewPen(w32.PS_GEOMETRIC, 2, ImageBoxMarkBrush) + +type ImageBox struct { + Name string + Type int + X, Y, X2, Y2 int + + underMouse bool // dynamic value +} + +func (b *ImageBox) Rect() *Rect { + return NewRect(b.X, b.Y, b.X2, b.Y2) +} + +// ImageViewBox is image view with boxes. +type ImageViewBox struct { + ControlBase + + bmp *Bitmap + mouseLeft bool + modified bool // used by GUI to see if any image box modified + + add bool + + Boxes []*ImageBox // might be persisted to file + dragBox *ImageBox + selBox *ImageBox + + dragStartX, dragStartY int + resize direction + + onSelectedChange EventManager + onAdd EventManager + onModify EventManager +} + +func NewImageViewBox(parent Controller) *ImageViewBox { + iv := new(ImageViewBox) + + iv.InitWindow("winc_ImageViewBox", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + RegMsgHandler(iv) + + iv.SetFont(DefaultFont) + iv.SetText("") + iv.SetSize(200, 65) + + return iv +} + +func (iv *ImageViewBox) OnSelectedChange() *EventManager { + return &iv.onSelectedChange +} + +func (iv *ImageViewBox) OnAdd() *EventManager { + return &iv.onAdd +} + +func (iv *ImageViewBox) OnModify() *EventManager { + return &iv.onModify +} + +func (iv *ImageViewBox) IsModified() bool { return iv.modified } +func (iv *ImageViewBox) SetModified(modified bool) { iv.modified = modified } +func (iv *ImageViewBox) IsLoaded() bool { return iv.bmp != nil } +func (iv *ImageViewBox) AddMode() bool { return iv.add } +func (iv *ImageViewBox) SetAddMode(add bool) { iv.add = add } +func (iv *ImageViewBox) HasSelected() bool { return iv.selBox != nil && iv.bmp != nil } + +func (iv *ImageViewBox) wasModified() { + iv.modified = true + iv.onModify.Fire(NewEvent(iv, nil)) +} + +func (iv *ImageViewBox) DeleteSelected() { + if iv.selBox != nil { + for i, b := range iv.Boxes { + if b == iv.selBox { + iv.Boxes = append(iv.Boxes[:i], iv.Boxes[i+1:]...) + iv.selBox = nil + iv.Invalidate(true) + iv.wasModified() + iv.onSelectedChange.Fire(NewEvent(iv, nil)) + return + } + } + } +} + +func (iv *ImageViewBox) NameSelected() string { + if iv.selBox != nil { + return iv.selBox.Name + } + return "" +} + +func (iv *ImageViewBox) SetNameSelected(name string) { + if iv.selBox != nil { + iv.selBox.Name = name + iv.wasModified() + } +} + +func (iv *ImageViewBox) TypeSelected() int { + if iv.selBox != nil { + return iv.selBox.Type + } + return 0 +} + +func (iv *ImageViewBox) SetTypeSelected(typ int) { + if iv.selBox != nil { + iv.selBox.Type = typ + iv.wasModified() + } +} + +func (ib *ImageViewBox) updateHighlight(x, y int) bool { + var changed bool + for _, b := range ib.Boxes { + under := x >= b.X && y >= b.Y && x <= b.X2 && y <= b.Y2 + if b.underMouse != under { + changed = true + } + b.underMouse = under + /*if sel { + break // allow only one to be underMouse + }*/ + } + return changed +} + +func (ib *ImageViewBox) isUnderMouse(x, y int) *ImageBox { + for _, b := range ib.Boxes { + if x >= b.X && y >= b.Y && x <= b.X2 && y <= b.Y2 { + return b + } + } + return nil +} + +func (ib *ImageViewBox) getCursor(x, y int) uint16 { + for _, b := range ib.Boxes { + switch d := ib.resizingDirection(b, x, y); d { + case DirY, DirY2: + return w32.IDC_SIZENS + case DirX, DirX2: + return w32.IDC_SIZEWE + } + // w32.IDC_SIZEALL or w32.IDC_SIZE for resize + } + return w32.IDC_ARROW +} + +func (ib *ImageViewBox) resizingDirection(b *ImageBox, x, y int) direction { + if b == nil { + return DirNone + } + switch { + case b.X == x || b.X == x-1 || b.X == x+1: + return DirX + case b.X2 == x || b.X2 == x-1 || b.X2 == x+1: + return DirX2 + case b.Y == y || b.Y == y-1 || b.Y == y+1: + return DirY + case b.Y2 == y || b.Y2 == y-1 || b.Y2 == y+1: + return DirY2 + } + return DirNone +} + +func (ib *ImageViewBox) resizeToDirection(b *ImageBox, x, y int) { + switch ib.resize { + case DirX: + b.X = x + case DirY: + b.Y = y + case DirX2: + b.X2 = x + case DirY2: + b.Y2 = y + } +} + +func (ib *ImageViewBox) drag(b *ImageBox, x, y int) { + w, h := b.X2-b.X, b.Y2-b.Y + + nx := ib.dragStartX - b.X + ny := ib.dragStartY - b.Y + + b.X = x - nx + b.Y = y - ny + b.X2 = b.X + w + b.Y2 = b.Y + h + + ib.dragStartX, ib.dragStartY = x, y +} + +func (iv *ImageViewBox) DrawImageFile(filepath string) (err error) { + iv.bmp, err = NewBitmapFromFile(filepath, RGB(255, 255, 0)) + iv.selBox = nil + iv.modified = false + iv.onSelectedChange.Fire(NewEvent(iv, nil)) + iv.onModify.Fire(NewEvent(iv, nil)) + return +} + +func (iv *ImageViewBox) DrawImage(bmp *Bitmap) { + iv.bmp = bmp + iv.selBox = nil + iv.modified = false + iv.onSelectedChange.Fire(NewEvent(iv, nil)) + iv.onModify.Fire(NewEvent(iv, nil)) +} + +func (iv *ImageViewBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_SIZE, w32.WM_SIZING: + iv.Invalidate(true) + + case w32.WM_ERASEBKGND: + return 1 // important + + case w32.WM_CREATE: + internalTrackMouseEvent(iv.hwnd) + + case w32.WM_PAINT: + if iv.bmp != nil { + canvas := NewCanvasFromHwnd(iv.hwnd) + defer canvas.Dispose() + iv.SetSize(iv.bmp.Size()) + canvas.DrawBitmap(iv.bmp, 0, 0) + + for _, b := range iv.Boxes { + // old code used NewSystemColorBrush(w32.COLOR_BTNFACE) w32.COLOR_WINDOW + pen := ImageBoxPen + if b.underMouse { + pen = ImageBoxHiPen + } + canvas.DrawRect(b.Rect(), pen) + + if b == iv.selBox { + x1 := []int{b.X, b.X2, b.X2, b.X} + y1 := []int{b.Y, b.Y, b.Y2, b.Y2} + + for i := 0; i < len(x1); i++ { + r := NewRect(x1[i]-2, y1[i]-2, x1[i]+2, y1[i]+2) + canvas.DrawFillRect(r, ImageBoxMarkPen, ImageBoxMarkBrush) + } + + } + } + } + + case w32.WM_MOUSEMOVE: + x, y := genPoint(lparam) + + if iv.dragBox != nil { + if iv.resize == DirNone { + iv.drag(iv.dragBox, x, y) + iv.wasModified() + } else { + iv.resizeToDirection(iv.dragBox, x, y) + iv.wasModified() + } + iv.Invalidate(true) + + } else { + if !iv.add { + w32.SetCursor(w32.LoadCursorWithResourceID(0, iv.getCursor(x, y))) + } + // do not call repaint if underMouse item did not change. + if iv.updateHighlight(x, y) { + iv.Invalidate(true) + } + } + + if iv.mouseLeft { + internalTrackMouseEvent(iv.hwnd) + iv.mouseLeft = false + } + + case w32.WM_MOUSELEAVE: + iv.dragBox = nil + iv.mouseLeft = true + iv.updateHighlight(-1, -1) + iv.Invalidate(true) + + case w32.WM_LBUTTONUP: + iv.dragBox = nil + + case w32.WM_LBUTTONDOWN: + x, y := genPoint(lparam) + if iv.add { + now := time.Now() + s := fmt.Sprintf("field%s", now.Format("020405")) + b := &ImageBox{Name: s, underMouse: true, X: x, Y: y, X2: x + 150, Y2: y + 30} + iv.Boxes = append(iv.Boxes, b) + iv.selBox = b + iv.wasModified() + iv.onAdd.Fire(NewEvent(iv, nil)) + } else { + iv.dragBox = iv.isUnderMouse(x, y) + iv.selBox = iv.dragBox + iv.dragStartX, iv.dragStartY = x, y + iv.resize = iv.resizingDirection(iv.dragBox, x, y) + } + iv.Invalidate(true) + iv.onSelectedChange.Fire(NewEvent(iv, nil)) + + case w32.WM_RBUTTONDOWN: + + } + return w32.DefWindowProc(iv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/init.go b/v2/internal/frontend/desktop/windows/winc/init.go new file mode 100644 index 000000000..b0037f5aa --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/init.go @@ -0,0 +1,21 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func init() { + gControllerRegistry = make(map[w32.HWND]Controller) + gRegisteredClasses = make([]string, 0) + + var si w32.GdiplusStartupInput + si.GdiplusVersion = 1 + w32.GdiplusStartup(&si, nil) +} diff --git a/v2/internal/frontend/desktop/windows/winc/keyboard.go b/v2/internal/frontend/desktop/windows/winc/keyboard.go new file mode 100644 index 000000000..1f6369240 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/keyboard.go @@ -0,0 +1,440 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "bytes" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Key uint16 + +func (k Key) String() string { + return key2string[k] +} + +const ( + KeyLButton Key = w32.VK_LBUTTON + KeyRButton Key = w32.VK_RBUTTON + KeyCancel Key = w32.VK_CANCEL + KeyMButton Key = w32.VK_MBUTTON + KeyXButton1 Key = w32.VK_XBUTTON1 + KeyXButton2 Key = w32.VK_XBUTTON2 + KeyBack Key = w32.VK_BACK + KeyTab Key = w32.VK_TAB + KeyClear Key = w32.VK_CLEAR + KeyReturn Key = w32.VK_RETURN + KeyShift Key = w32.VK_SHIFT + KeyControl Key = w32.VK_CONTROL + KeyAlt Key = w32.VK_MENU + KeyMenu Key = w32.VK_MENU + KeyPause Key = w32.VK_PAUSE + KeyCapital Key = w32.VK_CAPITAL + KeyKana Key = w32.VK_KANA + KeyHangul Key = w32.VK_HANGUL + KeyJunja Key = w32.VK_JUNJA + KeyFinal Key = w32.VK_FINAL + KeyHanja Key = w32.VK_HANJA + KeyKanji Key = w32.VK_KANJI + KeyEscape Key = w32.VK_ESCAPE + KeyConvert Key = w32.VK_CONVERT + KeyNonconvert Key = w32.VK_NONCONVERT + KeyAccept Key = w32.VK_ACCEPT + KeyModeChange Key = w32.VK_MODECHANGE + KeySpace Key = w32.VK_SPACE + KeyPrior Key = w32.VK_PRIOR + KeyNext Key = w32.VK_NEXT + KeyEnd Key = w32.VK_END + KeyHome Key = w32.VK_HOME + KeyLeft Key = w32.VK_LEFT + KeyUp Key = w32.VK_UP + KeyRight Key = w32.VK_RIGHT + KeyDown Key = w32.VK_DOWN + KeySelect Key = w32.VK_SELECT + KeyPrint Key = w32.VK_PRINT + KeyExecute Key = w32.VK_EXECUTE + KeySnapshot Key = w32.VK_SNAPSHOT + KeyInsert Key = w32.VK_INSERT + KeyDelete Key = w32.VK_DELETE + KeyHelp Key = w32.VK_HELP + Key0 Key = 0x30 + Key1 Key = 0x31 + Key2 Key = 0x32 + Key3 Key = 0x33 + Key4 Key = 0x34 + Key5 Key = 0x35 + Key6 Key = 0x36 + Key7 Key = 0x37 + Key8 Key = 0x38 + Key9 Key = 0x39 + KeyA Key = 0x41 + KeyB Key = 0x42 + KeyC Key = 0x43 + KeyD Key = 0x44 + KeyE Key = 0x45 + KeyF Key = 0x46 + KeyG Key = 0x47 + KeyH Key = 0x48 + KeyI Key = 0x49 + KeyJ Key = 0x4A + KeyK Key = 0x4B + KeyL Key = 0x4C + KeyM Key = 0x4D + KeyN Key = 0x4E + KeyO Key = 0x4F + KeyP Key = 0x50 + KeyQ Key = 0x51 + KeyR Key = 0x52 + KeyS Key = 0x53 + KeyT Key = 0x54 + KeyU Key = 0x55 + KeyV Key = 0x56 + KeyW Key = 0x57 + KeyX Key = 0x58 + KeyY Key = 0x59 + KeyZ Key = 0x5A + KeyLWIN Key = w32.VK_LWIN + KeyRWIN Key = w32.VK_RWIN + KeyApps Key = w32.VK_APPS + KeySleep Key = w32.VK_SLEEP + KeyNumpad0 Key = w32.VK_NUMPAD0 + KeyNumpad1 Key = w32.VK_NUMPAD1 + KeyNumpad2 Key = w32.VK_NUMPAD2 + KeyNumpad3 Key = w32.VK_NUMPAD3 + KeyNumpad4 Key = w32.VK_NUMPAD4 + KeyNumpad5 Key = w32.VK_NUMPAD5 + KeyNumpad6 Key = w32.VK_NUMPAD6 + KeyNumpad7 Key = w32.VK_NUMPAD7 + KeyNumpad8 Key = w32.VK_NUMPAD8 + KeyNumpad9 Key = w32.VK_NUMPAD9 + KeyMultiply Key = w32.VK_MULTIPLY + KeyAdd Key = w32.VK_ADD + KeySeparator Key = w32.VK_SEPARATOR + KeySubtract Key = w32.VK_SUBTRACT + KeyDecimal Key = w32.VK_DECIMAL + KeyDivide Key = w32.VK_DIVIDE + KeyF1 Key = w32.VK_F1 + KeyF2 Key = w32.VK_F2 + KeyF3 Key = w32.VK_F3 + KeyF4 Key = w32.VK_F4 + KeyF5 Key = w32.VK_F5 + KeyF6 Key = w32.VK_F6 + KeyF7 Key = w32.VK_F7 + KeyF8 Key = w32.VK_F8 + KeyF9 Key = w32.VK_F9 + KeyF10 Key = w32.VK_F10 + KeyF11 Key = w32.VK_F11 + KeyF12 Key = w32.VK_F12 + KeyF13 Key = w32.VK_F13 + KeyF14 Key = w32.VK_F14 + KeyF15 Key = w32.VK_F15 + KeyF16 Key = w32.VK_F16 + KeyF17 Key = w32.VK_F17 + KeyF18 Key = w32.VK_F18 + KeyF19 Key = w32.VK_F19 + KeyF20 Key = w32.VK_F20 + KeyF21 Key = w32.VK_F21 + KeyF22 Key = w32.VK_F22 + KeyF23 Key = w32.VK_F23 + KeyF24 Key = w32.VK_F24 + KeyNumlock Key = w32.VK_NUMLOCK + KeyScroll Key = w32.VK_SCROLL + KeyLShift Key = w32.VK_LSHIFT + KeyRShift Key = w32.VK_RSHIFT + KeyLControl Key = w32.VK_LCONTROL + KeyRControl Key = w32.VK_RCONTROL + KeyLAlt Key = w32.VK_LMENU + KeyLMenu Key = w32.VK_LMENU + KeyRAlt Key = w32.VK_RMENU + KeyRMenu Key = w32.VK_RMENU + KeyBrowserBack Key = w32.VK_BROWSER_BACK + KeyBrowserForward Key = w32.VK_BROWSER_FORWARD + KeyBrowserRefresh Key = w32.VK_BROWSER_REFRESH + KeyBrowserStop Key = w32.VK_BROWSER_STOP + KeyBrowserSearch Key = w32.VK_BROWSER_SEARCH + KeyBrowserFavorites Key = w32.VK_BROWSER_FAVORITES + KeyBrowserHome Key = w32.VK_BROWSER_HOME + KeyVolumeMute Key = w32.VK_VOLUME_MUTE + KeyVolumeDown Key = w32.VK_VOLUME_DOWN + KeyVolumeUp Key = w32.VK_VOLUME_UP + KeyMediaNextTrack Key = w32.VK_MEDIA_NEXT_TRACK + KeyMediaPrevTrack Key = w32.VK_MEDIA_PREV_TRACK + KeyMediaStop Key = w32.VK_MEDIA_STOP + KeyMediaPlayPause Key = w32.VK_MEDIA_PLAY_PAUSE + KeyLaunchMail Key = w32.VK_LAUNCH_MAIL + KeyLaunchMediaSelect Key = w32.VK_LAUNCH_MEDIA_SELECT + KeyLaunchApp1 Key = w32.VK_LAUNCH_APP1 + KeyLaunchApp2 Key = w32.VK_LAUNCH_APP2 + KeyOEM1 Key = w32.VK_OEM_1 + KeyOEMPlus Key = w32.VK_OEM_PLUS + KeyOEMComma Key = w32.VK_OEM_COMMA + KeyOEMMinus Key = w32.VK_OEM_MINUS + KeyOEMPeriod Key = w32.VK_OEM_PERIOD + KeyOEM2 Key = w32.VK_OEM_2 + KeyOEM3 Key = w32.VK_OEM_3 + KeyOEM4 Key = w32.VK_OEM_4 + KeyOEM5 Key = w32.VK_OEM_5 + KeyOEM6 Key = w32.VK_OEM_6 + KeyOEM7 Key = w32.VK_OEM_7 + KeyOEM8 Key = w32.VK_OEM_8 + KeyOEM102 Key = w32.VK_OEM_102 + KeyProcessKey Key = w32.VK_PROCESSKEY + KeyPacket Key = w32.VK_PACKET + KeyAttn Key = w32.VK_ATTN + KeyCRSel Key = w32.VK_CRSEL + KeyEXSel Key = w32.VK_EXSEL + KeyErEOF Key = w32.VK_EREOF + KeyPlay Key = w32.VK_PLAY + KeyZoom Key = w32.VK_ZOOM + KeyNoName Key = w32.VK_NONAME + KeyPA1 Key = w32.VK_PA1 + KeyOEMClear Key = w32.VK_OEM_CLEAR +) + +var key2string = map[Key]string{ + KeyLButton: "LButton", + KeyRButton: "RButton", + KeyCancel: "Cancel", + KeyMButton: "MButton", + KeyXButton1: "XButton1", + KeyXButton2: "XButton2", + KeyBack: "Back", + KeyTab: "Tab", + KeyClear: "Clear", + KeyReturn: "Return", + KeyShift: "Shift", + KeyControl: "Control", + KeyAlt: "Alt / Menu", + KeyPause: "Pause", + KeyCapital: "Capital", + KeyKana: "Kana / Hangul", + KeyJunja: "Junja", + KeyFinal: "Final", + KeyHanja: "Hanja / Kanji", + KeyEscape: "Escape", + KeyConvert: "Convert", + KeyNonconvert: "Nonconvert", + KeyAccept: "Accept", + KeyModeChange: "ModeChange", + KeySpace: "Space", + KeyPrior: "Prior", + KeyNext: "Next", + KeyEnd: "End", + KeyHome: "Home", + KeyLeft: "Left", + KeyUp: "Up", + KeyRight: "Right", + KeyDown: "Down", + KeySelect: "Select", + KeyPrint: "Print", + KeyExecute: "Execute", + KeySnapshot: "Snapshot", + KeyInsert: "Insert", + KeyDelete: "Delete", + KeyHelp: "Help", + Key0: "0", + Key1: "1", + Key2: "2", + Key3: "3", + Key4: "4", + Key5: "5", + Key6: "6", + Key7: "7", + Key8: "8", + Key9: "9", + KeyA: "A", + KeyB: "B", + KeyC: "C", + KeyD: "D", + KeyE: "E", + KeyF: "F", + KeyG: "G", + KeyH: "H", + KeyI: "I", + KeyJ: "J", + KeyK: "K", + KeyL: "L", + KeyM: "M", + KeyN: "N", + KeyO: "O", + KeyP: "P", + KeyQ: "Q", + KeyR: "R", + KeyS: "S", + KeyT: "T", + KeyU: "U", + KeyV: "V", + KeyW: "W", + KeyX: "X", + KeyY: "Y", + KeyZ: "Z", + KeyLWIN: "LWIN", + KeyRWIN: "RWIN", + KeyApps: "Apps", + KeySleep: "Sleep", + KeyNumpad0: "Numpad0", + KeyNumpad1: "Numpad1", + KeyNumpad2: "Numpad2", + KeyNumpad3: "Numpad3", + KeyNumpad4: "Numpad4", + KeyNumpad5: "Numpad5", + KeyNumpad6: "Numpad6", + KeyNumpad7: "Numpad7", + KeyNumpad8: "Numpad8", + KeyNumpad9: "Numpad9", + KeyMultiply: "Multiply", + KeyAdd: "Add", + KeySeparator: "Separator", + KeySubtract: "Subtract", + KeyDecimal: "Decimal", + KeyDivide: "Divide", + KeyF1: "F1", + KeyF2: "F2", + KeyF3: "F3", + KeyF4: "F4", + KeyF5: "F5", + KeyF6: "F6", + KeyF7: "F7", + KeyF8: "F8", + KeyF9: "F9", + KeyF10: "F10", + KeyF11: "F11", + KeyF12: "F12", + KeyF13: "F13", + KeyF14: "F14", + KeyF15: "F15", + KeyF16: "F16", + KeyF17: "F17", + KeyF18: "F18", + KeyF19: "F19", + KeyF20: "F20", + KeyF21: "F21", + KeyF22: "F22", + KeyF23: "F23", + KeyF24: "F24", + KeyNumlock: "Numlock", + KeyScroll: "Scroll", + KeyLShift: "LShift", + KeyRShift: "RShift", + KeyLControl: "LControl", + KeyRControl: "RControl", + KeyLMenu: "LMenu", + KeyRMenu: "RMenu", + KeyBrowserBack: "BrowserBack", + KeyBrowserForward: "BrowserForward", + KeyBrowserRefresh: "BrowserRefresh", + KeyBrowserStop: "BrowserStop", + KeyBrowserSearch: "BrowserSearch", + KeyBrowserFavorites: "BrowserFavorites", + KeyBrowserHome: "BrowserHome", + KeyVolumeMute: "VolumeMute", + KeyVolumeDown: "VolumeDown", + KeyVolumeUp: "VolumeUp", + KeyMediaNextTrack: "MediaNextTrack", + KeyMediaPrevTrack: "MediaPrevTrack", + KeyMediaStop: "MediaStop", + KeyMediaPlayPause: "MediaPlayPause", + KeyLaunchMail: "LaunchMail", + KeyLaunchMediaSelect: "LaunchMediaSelect", + KeyLaunchApp1: "LaunchApp1", + KeyLaunchApp2: "LaunchApp2", + KeyOEM1: "OEM1", + KeyOEMPlus: "OEMPlus", + KeyOEMComma: "OEMComma", + KeyOEMMinus: "OEMMinus", + KeyOEMPeriod: "OEMPeriod", + KeyOEM2: "OEM2", + KeyOEM3: "OEM3", + KeyOEM4: "OEM4", + KeyOEM5: "OEM5", + KeyOEM6: "OEM6", + KeyOEM7: "OEM7", + KeyOEM8: "OEM8", + KeyOEM102: "OEM102", + KeyProcessKey: "ProcessKey", + KeyPacket: "Packet", + KeyAttn: "Attn", + KeyCRSel: "CRSel", + KeyEXSel: "EXSel", + KeyErEOF: "ErEOF", + KeyPlay: "Play", + KeyZoom: "Zoom", + KeyNoName: "NoName", + KeyPA1: "PA1", + KeyOEMClear: "OEMClear", +} + +type Modifiers byte + +func (m Modifiers) String() string { + return modifiers2string[m] +} + +var modifiers2string = map[Modifiers]string{ + ModShift: "Shift", + ModControl: "Ctrl", + ModControl | ModShift: "Ctrl+Shift", + ModAlt: "Alt", + ModAlt | ModShift: "Alt+Shift", + ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift", +} + +const ( + ModShift Modifiers = 1 << iota + ModControl + ModAlt +) + +func ModifiersDown() Modifiers { + var m Modifiers + + if ShiftDown() { + m |= ModShift + } + if ControlDown() { + m |= ModControl + } + if AltDown() { + m |= ModAlt + } + + return m +} + +type Shortcut struct { + Modifiers Modifiers + Key Key +} + +func (s Shortcut) String() string { + m := s.Modifiers.String() + if m == "" { + return s.Key.String() + } + + b := new(bytes.Buffer) + + b.WriteString(m) + b.WriteRune('+') + b.WriteString(s.Key.String()) + + return b.String() +} + +func AltDown() bool { + return w32.GetKeyState(int32(KeyAlt))>>15 != 0 +} + +func ControlDown() bool { + return w32.GetKeyState(int32(KeyControl))>>15 != 0 +} + +func ShiftDown() bool { + return w32.GetKeyState(int32(KeyShift))>>15 != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/label.go b/v2/internal/frontend/desktop/windows/winc/label.go new file mode 100644 index 000000000..6e441e9e2 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/label.go @@ -0,0 +1,31 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Label struct { + ControlBase +} + +func NewLabel(parent Controller) *Label { + lb := new(Label) + + lb.InitControl("STATIC", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.SS_LEFTNOWORDWRAP) + RegMsgHandler(lb) + + lb.SetFont(DefaultFont) + lb.SetText("Label") + lb.SetSize(100, 25) + return lb +} + +func (lb *Label) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + return w32.DefWindowProc(lb.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/layout.go b/v2/internal/frontend/desktop/windows/winc/layout.go new file mode 100644 index 000000000..7962dc726 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/layout.go @@ -0,0 +1,222 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "encoding/json" + "fmt" + "io" + "os" + "sort" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// Dockable component must satisfy interface to be docked. +type Dockable interface { + Handle() w32.HWND + + Pos() (x, y int) + Width() int + Height() int + Visible() bool + + SetPos(x, y int) + SetSize(width, height int) + + OnMouseMove() *EventManager + OnLBUp() *EventManager +} + +// DockAllow is window, panel or other component that satisfies interface. +type DockAllow interface { + Handle() w32.HWND + ClientWidth() int + ClientHeight() int + SetLayout(mng LayoutManager) +} + +// Various layout managers +type Direction int + +const ( + Top Direction = iota + Bottom + Left + Right + Fill +) + +type LayoutControl struct { + child Dockable + dir Direction +} + +type LayoutControls []*LayoutControl + +type SimpleDock struct { + parent DockAllow + layoutCtl LayoutControls + loadedState bool +} + +// CtlState gets saved and loaded from json +type CtlState struct { + X, Y, Width, Height int +} + +type LayoutState struct { + WindowState string + Controls []*CtlState +} + +func (lc LayoutControls) Len() int { return len(lc) } +func (lc LayoutControls) Swap(i, j int) { lc[i], lc[j] = lc[j], lc[i] } +func (lc LayoutControls) Less(i, j int) bool { return lc[i].dir < lc[j].dir } + +func NewSimpleDock(parent DockAllow) *SimpleDock { + d := &SimpleDock{parent: parent} + parent.SetLayout(d) + return d +} + +// Layout management for the child controls. +func (sd *SimpleDock) Dock(child Dockable, dir Direction) { + sd.layoutCtl = append(sd.layoutCtl, &LayoutControl{child, dir}) +} + +// SaveState of the layout. Only works for Docks with parent set to main form. +func (sd *SimpleDock) SaveState(w io.Writer) error { + var ls LayoutState + + var wp w32.WINDOWPLACEMENT + wp.Length = uint32(unsafe.Sizeof(wp)) + if !w32.GetWindowPlacement(sd.parent.Handle(), &wp) { + return fmt.Errorf("GetWindowPlacement failed") + } + + ls.WindowState = fmt.Sprint( + wp.Flags, wp.ShowCmd, + wp.PtMinPosition.X, wp.PtMinPosition.Y, + wp.PtMaxPosition.X, wp.PtMaxPosition.Y, + wp.RcNormalPosition.Left, wp.RcNormalPosition.Top, + wp.RcNormalPosition.Right, wp.RcNormalPosition.Bottom) + + for _, c := range sd.layoutCtl { + x, y := c.child.Pos() + w, h := c.child.Width(), c.child.Height() + + ctl := &CtlState{X: x, Y: y, Width: w, Height: h} + ls.Controls = append(ls.Controls, ctl) + } + + if err := json.NewEncoder(w).Encode(ls); err != nil { + return err + } + + return nil +} + +// LoadState of the layout. Only works for Docks with parent set to main form. +func (sd *SimpleDock) LoadState(r io.Reader) error { + var ls LayoutState + + if err := json.NewDecoder(r).Decode(&ls); err != nil { + return err + } + + var wp w32.WINDOWPLACEMENT + if _, err := fmt.Sscan(ls.WindowState, + &wp.Flags, &wp.ShowCmd, + &wp.PtMinPosition.X, &wp.PtMinPosition.Y, + &wp.PtMaxPosition.X, &wp.PtMaxPosition.Y, + &wp.RcNormalPosition.Left, &wp.RcNormalPosition.Top, + &wp.RcNormalPosition.Right, &wp.RcNormalPosition.Bottom); err != nil { + return err + } + wp.Length = uint32(unsafe.Sizeof(wp)) + + if !w32.SetWindowPlacement(sd.parent.Handle(), &wp) { + return fmt.Errorf("SetWindowPlacement failed") + } + + // if number of controls in the saved layout does not match + // current number on screen - something changed and we do not reload + // rest of control sizes from json + if len(sd.layoutCtl) != len(ls.Controls) { + return nil + } + + for i, c := range sd.layoutCtl { + c.child.SetPos(ls.Controls[i].X, ls.Controls[i].Y) + c.child.SetSize(ls.Controls[i].Width, ls.Controls[i].Height) + } + return nil +} + +// SaveStateFile convenience function. +func (sd *SimpleDock) SaveStateFile(file string) error { + f, err := os.Create(file) + if err != nil { + return err + } + return sd.SaveState(f) +} + +// LoadStateFile loads state ignores error if file is not found. +func (sd *SimpleDock) LoadStateFile(file string) error { + f, err := os.Open(file) + if err != nil { + return nil // if file is not found or not accessible ignore it + } + return sd.LoadState(f) +} + +// Update is called to resize child items based on layout directions. +func (sd *SimpleDock) Update() { + sort.Stable(sd.layoutCtl) + + x, y := 0, 0 + w, h := sd.parent.ClientWidth(), sd.parent.ClientHeight() + winw, winh := w, h + + for _, c := range sd.layoutCtl { + // Non visible controls do not preserve space. + if !c.child.Visible() { + continue + } + + switch c.dir { + case Top: + c.child.SetPos(x, y) + c.child.SetSize(w, c.child.Height()) + h -= c.child.Height() + y += c.child.Height() + case Bottom: + c.child.SetPos(x, winh-c.child.Height()) + c.child.SetSize(w, c.child.Height()) + h -= c.child.Height() + winh -= c.child.Height() + case Left: + c.child.SetPos(x, y) + c.child.SetSize(c.child.Width(), h) + w -= c.child.Width() + x += c.child.Width() + case Right: + c.child.SetPos(winw-c.child.Width(), y) + c.child.SetSize(c.child.Width(), h) + w -= c.child.Width() + winw -= c.child.Width() + case Fill: + // fill available space + c.child.SetPos(x, y) + c.child.SetSize(w, h) + } + //c.child.Invalidate(true) + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/listview.go b/v2/internal/frontend/desktop/windows/winc/listview.go new file mode 100644 index 000000000..8edfd1c11 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/listview.go @@ -0,0 +1,549 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "errors" + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// ListItem represents an item in a ListView widget. +type ListItem interface { + Text() []string // Text returns the text of the multi-column item. + ImageIndex() int // ImageIndex is used only if SetImageList is called on the listview +} + +// ListItemChecker is used for checkbox support in ListView. +type ListItemChecker interface { + Checked() bool + SetChecked(checked bool) +} + +// ListItemSetter is used in OnEndLabelEdit event. +type ListItemSetter interface { + SetText(s string) // set first item in the array via LabelEdit event +} + +// StringListItem is helper for basic string lists. +type StringListItem struct { + ID int + Data string + Check bool +} + +func (s StringListItem) Text() []string { return []string{s.Data} } +func (s StringListItem) Checked() bool { return s.Check } +func (s StringListItem) SetChecked(checked bool) { s.Check = checked } +func (s StringListItem) ImageIndex() int { return 0 } + +type ListView struct { + ControlBase + + iml *ImageList + lastIndex int + cols int // count of columns + + item2Handle map[ListItem]uintptr + handle2Item map[uintptr]ListItem + + onEndLabelEdit EventManager + onDoubleClick EventManager + onClick EventManager + onKeyDown EventManager + onItemChanging EventManager + onItemChanged EventManager + onCheckChanged EventManager + onViewChange EventManager + onEndScroll EventManager +} + +func NewListView(parent Controller) *ListView { + lv := new(ListView) + + lv.InitControl("SysListView32", parent /*w32.WS_EX_CLIENTEDGE*/, 0, + w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.LVS_REPORT|w32.LVS_EDITLABELS|w32.LVS_SHOWSELALWAYS) + + lv.item2Handle = make(map[ListItem]uintptr) + lv.handle2Item = make(map[uintptr]ListItem) + + RegMsgHandler(lv) + + lv.SetFont(DefaultFont) + lv.SetSize(200, 400) + + if err := lv.SetTheme("Explorer"); err != nil { + // theme error is ignored + } + return lv +} + +// FIXME: Changes the state of an item in a list-view control. Refer LVM_SETITEMSTATE message. +func (lv *ListView) setItemState(i int, state, mask uint) { + var item w32.LVITEM + item.State, item.StateMask = uint32(state), uint32(mask) + w32.SendMessage(lv.hwnd, w32.LVM_SETITEMSTATE, uintptr(i), uintptr(unsafe.Pointer(&item))) +} + +func (lv *ListView) EnableSingleSelect(enable bool) { + SetStyle(lv.hwnd, enable, w32.LVS_SINGLESEL) +} + +func (lv *ListView) EnableSortHeader(enable bool) { + SetStyle(lv.hwnd, enable, w32.LVS_NOSORTHEADER) +} + +func (lv *ListView) EnableSortAscending(enable bool) { + SetStyle(lv.hwnd, enable, w32.LVS_SORTASCENDING) +} + +func (lv *ListView) EnableEditLabels(enable bool) { + SetStyle(lv.hwnd, enable, w32.LVS_EDITLABELS) +} + +func (lv *ListView) EnableFullRowSelect(enable bool) { + if enable { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_FULLROWSELECT) + } else { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_FULLROWSELECT, 0) + } +} + +func (lv *ListView) EnableDoubleBuffer(enable bool) { + if enable { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_DOUBLEBUFFER) + } else { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_DOUBLEBUFFER, 0) + } +} + +func (lv *ListView) EnableHotTrack(enable bool) { + if enable { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_TRACKSELECT) + } else { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_TRACKSELECT, 0) + } +} + +func (lv *ListView) SetItemCount(count int) bool { + return w32.SendMessage(lv.hwnd, w32.LVM_SETITEMCOUNT, uintptr(count), 0) != 0 +} + +func (lv *ListView) ItemCount() int { + return int(w32.SendMessage(lv.hwnd, w32.LVM_GETITEMCOUNT, 0, 0)) +} + +func (lv *ListView) ItemAt(x, y int) ListItem { + hti := w32.LVHITTESTINFO{Pt: w32.POINT{int32(x), int32(y)}} + w32.SendMessage(lv.hwnd, w32.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) + return lv.findItemByIndex(int(hti.IItem)) +} + +func (lv *ListView) Items() (list []ListItem) { + for item := range lv.item2Handle { + list = append(list, item) + } + return list +} + +func (lv *ListView) AddColumn(caption string, width int) { + var lc w32.LVCOLUMN + lc.Mask = w32.LVCF_TEXT + if width != 0 { + lc.Mask = lc.Mask | w32.LVCF_WIDTH + lc.Cx = int32(width) + } + lc.PszText = syscall.StringToUTF16Ptr(caption) + lv.insertLvColumn(&lc, lv.cols) + lv.cols++ +} + +// StretchLastColumn makes the last column take up all remaining horizontal +// space of the *ListView. +// The effect of this is not persistent. +func (lv *ListView) StretchLastColumn() error { + if lv.cols == 0 { + return nil + } + if w32.SendMessage(lv.hwnd, w32.LVM_SETCOLUMNWIDTH, uintptr(lv.cols-1), w32.LVSCW_AUTOSIZE_USEHEADER) == 0 { + //panic("LVM_SETCOLUMNWIDTH failed") + } + return nil +} + +// CheckBoxes returns if the *TableView has check boxes. +func (lv *ListView) CheckBoxes() bool { + return w32.SendMessage(lv.hwnd, w32.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)&w32.LVS_EX_CHECKBOXES > 0 +} + +// SetCheckBoxes sets if the *TableView has check boxes. +func (lv *ListView) SetCheckBoxes(value bool) { + exStyle := w32.SendMessage(lv.hwnd, w32.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) + oldStyle := exStyle + if value { + exStyle |= w32.LVS_EX_CHECKBOXES + } else { + exStyle &^= w32.LVS_EX_CHECKBOXES + } + if exStyle != oldStyle { + w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) + } + + mask := w32.SendMessage(lv.hwnd, w32.LVM_GETCALLBACKMASK, 0, 0) + if value { + mask |= w32.LVIS_STATEIMAGEMASK + } else { + mask &^= w32.LVIS_STATEIMAGEMASK + } + + if w32.SendMessage(lv.hwnd, w32.LVM_SETCALLBACKMASK, mask, 0) == w32.FALSE { + panic("SendMessage(LVM_SETCALLBACKMASK)") + } +} + +func (lv *ListView) applyImage(lc *w32.LVITEM, imIndex int) { + if lv.iml != nil { + lc.Mask |= w32.LVIF_IMAGE + lc.IImage = int32(imIndex) + } +} + +func (lv *ListView) AddItem(item ListItem) { + lv.InsertItem(item, lv.ItemCount()) +} + +func (lv *ListView) InsertItem(item ListItem, index int) { + text := item.Text() + li := &w32.LVITEM{ + Mask: w32.LVIF_TEXT | w32.LVIF_PARAM, + PszText: syscall.StringToUTF16Ptr(text[0]), + IItem: int32(index), + } + + lv.lastIndex++ + ix := new(int) + *ix = lv.lastIndex + li.LParam = uintptr(*ix) + lv.handle2Item[li.LParam] = item + lv.item2Handle[item] = li.LParam + + lv.applyImage(li, item.ImageIndex()) + lv.insertLvItem(li) + + for i := 1; i < len(text); i++ { + li.Mask = w32.LVIF_TEXT + li.PszText = syscall.StringToUTF16Ptr(text[i]) + li.ISubItem = int32(i) + lv.setLvItem(li) + } +} + +func (lv *ListView) UpdateItem(item ListItem) bool { + lparam, ok := lv.item2Handle[item] + if !ok { + return false + } + + index := lv.findIndexByItem(item) + if index == -1 { + return false + } + + text := item.Text() + li := &w32.LVITEM{ + Mask: w32.LVIF_TEXT | w32.LVIF_PARAM, + PszText: syscall.StringToUTF16Ptr(text[0]), + LParam: lparam, + IItem: int32(index), + } + + lv.applyImage(li, item.ImageIndex()) + lv.setLvItem(li) + + for i := 1; i < len(text); i++ { + li.Mask = w32.LVIF_TEXT + li.PszText = syscall.StringToUTF16Ptr(text[i]) + li.ISubItem = int32(i) + lv.setLvItem(li) + } + return true +} + +func (lv *ListView) insertLvColumn(lvColumn *w32.LVCOLUMN, iCol int) { + w32.SendMessage(lv.hwnd, w32.LVM_INSERTCOLUMN, uintptr(iCol), uintptr(unsafe.Pointer(lvColumn))) +} + +func (lv *ListView) insertLvItem(lvItem *w32.LVITEM) { + w32.SendMessage(lv.hwnd, w32.LVM_INSERTITEM, 0, uintptr(unsafe.Pointer(lvItem))) +} + +func (lv *ListView) setLvItem(lvItem *w32.LVITEM) { + w32.SendMessage(lv.hwnd, w32.LVM_SETITEM, 0, uintptr(unsafe.Pointer(lvItem))) +} + +func (lv *ListView) DeleteAllItems() bool { + if w32.SendMessage(lv.hwnd, w32.LVM_DELETEALLITEMS, 0, 0) == w32.TRUE { + lv.item2Handle = make(map[ListItem]uintptr) + lv.handle2Item = make(map[uintptr]ListItem) + return true + } + return false +} + +func (lv *ListView) DeleteItem(item ListItem) error { + index := lv.findIndexByItem(item) + if index == -1 { + return errors.New("item not found") + } + + if w32.SendMessage(lv.hwnd, w32.LVM_DELETEITEM, uintptr(index), 0) == 0 { + return errors.New("SendMessage(TVM_DELETEITEM) failed") + } + + h := lv.item2Handle[item] + delete(lv.item2Handle, item) + delete(lv.handle2Item, h) + return nil +} + +func (lv *ListView) findIndexByItem(item ListItem) int { + lparam, ok := lv.item2Handle[item] + if !ok { + return -1 + } + + it := &w32.LVFINDINFO{ + Flags: w32.LVFI_PARAM, + LParam: lparam, + } + var i int = -1 + return int(w32.SendMessage(lv.hwnd, w32.LVM_FINDITEM, uintptr(i), uintptr(unsafe.Pointer(it)))) +} + +func (lv *ListView) findItemByIndex(i int) ListItem { + it := &w32.LVITEM{ + Mask: w32.LVIF_PARAM, + IItem: int32(i), + } + + if w32.SendMessage(lv.hwnd, w32.LVM_GETITEM, 0, uintptr(unsafe.Pointer(it))) == w32.TRUE { + if item, ok := lv.handle2Item[it.LParam]; ok { + return item + } + } + return nil +} + +func (lv *ListView) EnsureVisible(item ListItem) bool { + if i := lv.findIndexByItem(item); i != -1 { + return w32.SendMessage(lv.hwnd, w32.LVM_ENSUREVISIBLE, uintptr(i), 1) == 0 + } + return false +} + +func (lv *ListView) SelectedItem() ListItem { + if items := lv.SelectedItems(); len(items) > 0 { + return items[0] + } + return nil +} + +func (lv *ListView) SetSelectedItem(item ListItem) bool { + if i := lv.findIndexByItem(item); i > -1 { + lv.SetSelectedIndex(i) + return true + } + return false +} + +// mask is used to set the LVITEM.Mask for ListView.GetItem which indicates which attributes you'd like to receive +// of LVITEM. +func (lv *ListView) SelectedItems() []ListItem { + var items []ListItem + + var i int = -1 + for { + if i = int(w32.SendMessage(lv.hwnd, w32.LVM_GETNEXTITEM, uintptr(i), uintptr(w32.LVNI_SELECTED))); i == -1 { + break + } + + if item := lv.findItemByIndex(i); item != nil { + items = append(items, item) + } + } + return items +} + +func (lv *ListView) SelectedCount() uint { + return uint(w32.SendMessage(lv.hwnd, w32.LVM_GETSELECTEDCOUNT, 0, 0)) +} + +// GetSelectedIndex first selected item index. Returns -1 if no item is selected. +func (lv *ListView) SelectedIndex() int { + var i int = -1 + return int(w32.SendMessage(lv.hwnd, w32.LVM_GETNEXTITEM, uintptr(i), uintptr(w32.LVNI_SELECTED))) +} + +// Set i to -1 to select all items. +func (lv *ListView) SetSelectedIndex(i int) { + lv.setItemState(i, w32.LVIS_SELECTED, w32.LVIS_SELECTED) +} + +func (lv *ListView) SetImageList(imageList *ImageList) { + w32.SendMessage(lv.hwnd, w32.LVM_SETIMAGELIST, w32.LVSIL_SMALL, uintptr(imageList.Handle())) + lv.iml = imageList +} + +// Event publishers +func (lv *ListView) OnEndLabelEdit() *EventManager { + return &lv.onEndLabelEdit +} + +func (lv *ListView) OnDoubleClick() *EventManager { + return &lv.onDoubleClick +} + +func (lv *ListView) OnClick() *EventManager { + return &lv.onClick +} + +func (lv *ListView) OnKeyDown() *EventManager { + return &lv.onKeyDown +} + +func (lv *ListView) OnItemChanging() *EventManager { + return &lv.onItemChanging +} + +func (lv *ListView) OnItemChanged() *EventManager { + return &lv.onItemChanged +} + +func (lv *ListView) OnCheckChanged() *EventManager { + return &lv.onCheckChanged +} + +func (lv *ListView) OnViewChange() *EventManager { + return &lv.onViewChange +} + +func (lv *ListView) OnEndScroll() *EventManager { + return &lv.onEndScroll +} + +// Message processor +func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + /*case w32.WM_ERASEBKGND: + lv.StretchLastColumn() + println("case w32.WM_ERASEBKGND") + return 1*/ + + case w32.WM_NOTIFY: + nm := (*w32.NMHDR)(unsafe.Pointer(lparam)) + code := int32(nm.Code) + + switch code { + case w32.LVN_BEGINLABELEDITW: + // println("Begin label edit") + case w32.LVN_ENDLABELEDITW: + nmdi := (*w32.NMLVDISPINFO)(unsafe.Pointer(lparam)) + if nmdi.Item.PszText != nil { + fmt.Println(nmdi.Item.PszText, nmdi.Item) + if item, ok := lv.handle2Item[nmdi.Item.LParam]; ok { + lv.onEndLabelEdit.Fire(NewEvent(lv, + &LabelEditEventData{Item: item, + Text: w32.UTF16PtrToString(nmdi.Item.PszText)})) + } + return w32.TRUE + } + case w32.NM_DBLCLK: + lv.onDoubleClick.Fire(NewEvent(lv, nil)) + + case w32.NM_CLICK: + ac := (*w32.NMITEMACTIVATE)(unsafe.Pointer(lparam)) + var hti w32.LVHITTESTINFO + hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y} + w32.SendMessage(lv.hwnd, w32.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) + + if hti.Flags == w32.LVHT_ONITEMSTATEICON { + if item := lv.findItemByIndex(int(hti.IItem)); item != nil { + if item, ok := item.(ListItemChecker); ok { + checked := !item.Checked() + item.SetChecked(checked) + lv.onCheckChanged.Fire(NewEvent(lv, item)) + + if w32.SendMessage(lv.hwnd, w32.LVM_UPDATE, uintptr(hti.IItem), 0) == w32.FALSE { + panic("SendMessage(LVM_UPDATE)") + } + } + } + } + + hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y} + w32.SendMessage(lv.hwnd, w32.LVM_SUBITEMHITTEST, 0, uintptr(unsafe.Pointer(&hti))) + lv.onClick.Fire(NewEvent(lv, hti.ISubItem)) + + case w32.LVN_KEYDOWN: + nmkey := (*w32.NMLVKEYDOWN)(unsafe.Pointer(lparam)) + if nmkey.WVKey == w32.VK_SPACE && lv.CheckBoxes() { + if item := lv.SelectedItem(); item != nil { + if item, ok := item.(ListItemChecker); ok { + checked := !item.Checked() + item.SetChecked(checked) + lv.onCheckChanged.Fire(NewEvent(lv, item)) + } + + index := lv.findIndexByItem(item) + if w32.SendMessage(lv.hwnd, w32.LVM_UPDATE, uintptr(index), 0) == w32.FALSE { + panic("SendMessage(LVM_UPDATE)") + } + } + } + lv.onKeyDown.Fire(NewEvent(lv, nmkey.WVKey)) + key := nmkey.WVKey + w32.SendMessage(lv.Parent().Handle(), w32.WM_KEYDOWN, uintptr(key), 0) + + case w32.LVN_ITEMCHANGING: + // This event also fires when listview has changed via code. + nmlv := (*w32.NMLISTVIEW)(unsafe.Pointer(lparam)) + item := lv.findItemByIndex(int(nmlv.IItem)) + lv.onItemChanging.Fire(NewEvent(lv, item)) + + case w32.LVN_ITEMCHANGED: + // This event also fires when listview has changed via code. + nmlv := (*w32.NMLISTVIEW)(unsafe.Pointer(lparam)) + item := lv.findItemByIndex(int(nmlv.IItem)) + lv.onItemChanged.Fire(NewEvent(lv, item)) + + case w32.LVN_GETDISPINFO: + nmdi := (*w32.NMLVDISPINFO)(unsafe.Pointer(lparam)) + if nmdi.Item.StateMask&w32.LVIS_STATEIMAGEMASK > 0 { + if item, ok := lv.handle2Item[nmdi.Item.LParam]; ok { + if item, ok := item.(ListItemChecker); ok { + + checked := item.Checked() + if checked { + nmdi.Item.State = 0x2000 + } else { + nmdi.Item.State = 0x1000 + } + } + } + } + + lv.onViewChange.Fire(NewEvent(lv, nil)) + + case w32.LVN_ENDSCROLL: + lv.onEndScroll.Fire(NewEvent(lv, nil)) + } + } + return w32.DefWindowProc(lv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/menu.go b/v2/internal/frontend/desktop/windows/winc/menu.go new file mode 100644 index 000000000..d1567e648 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/menu.go @@ -0,0 +1,339 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +var ( + nextMenuItemID uint16 = 3 + actionsByID = make(map[uint16]*MenuItem) + shortcut2Action = make(map[Shortcut]*MenuItem) + menuItems = make(map[w32.HMENU][]*MenuItem) + radioGroups = make(map[*MenuItem]*RadioGroup) + initialised bool +) + +var NoShortcut = Shortcut{} + +// Menu for main window and context menus on controls. +// Most methods used for both main window menu and context menu. +type Menu struct { + hMenu w32.HMENU + hwnd w32.HWND // hwnd might be nil if it is context menu. +} + +type MenuItem struct { + hMenu w32.HMENU + hSubMenu w32.HMENU // Non zero if this item is in itself a submenu. + + text string + toolTip string + image *Bitmap + shortcut Shortcut + enabled bool + + checkable bool + checked bool + isRadio bool + + id uint16 + + onClick EventManager +} + +type RadioGroup struct { + members []*MenuItem + hwnd w32.HWND +} + +func NewContextMenu() *MenuItem { + hMenu := w32.CreatePopupMenu() + if hMenu == 0 { + panic("failed CreateMenu") + } + + item := &MenuItem{ + hMenu: hMenu, + hSubMenu: hMenu, + } + return item +} + +func (m *Menu) Dispose() { + if m.hMenu != 0 { + w32.DestroyMenu(m.hMenu) + m.hMenu = 0 + } +} + +func (m *Menu) IsDisposed() bool { + return m.hMenu == 0 +} + +func initMenuItemInfoFromAction(mii *w32.MENUITEMINFO, a *MenuItem) { + mii.CbSize = uint32(unsafe.Sizeof(*mii)) + mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING + if a.image != nil { + mii.FMask |= w32.MIIM_BITMAP + mii.HbmpItem = a.image.handle + } + if a.IsSeparator() { + mii.FType = w32.MFT_SEPARATOR + } else { + mii.FType = w32.MFT_STRING + var text string + if s := a.shortcut; s.Key != 0 { + text = fmt.Sprintf("%s\t%s", a.text, s.String()) + shortcut2Action[a.shortcut] = a + } else { + text = a.text + } + mii.DwTypeData = syscall.StringToUTF16Ptr(text) + mii.Cch = uint32(len([]rune(a.text))) + } + mii.WID = uint32(a.id) + + if a.Enabled() { + mii.FState &^= w32.MFS_DISABLED + } else { + mii.FState |= w32.MFS_DISABLED + } + + if a.Checkable() { + mii.FMask |= w32.MIIM_CHECKMARKS + } + if a.Checked() { + mii.FState |= w32.MFS_CHECKED + } + + if a.hSubMenu != 0 { + mii.FMask |= w32.MIIM_SUBMENU + mii.HSubMenu = a.hSubMenu + } +} + +// Show menu on the main window. +func (m *Menu) Show() { + initialised = true + updateRadioGroups() + if !w32.DrawMenuBar(m.hwnd) { + panic("DrawMenuBar failed") + } +} + +// AddSubMenu returns item that is used as submenu to perform AddItem(s). +func (m *Menu) AddSubMenu(text string) *MenuItem { + hSubMenu := w32.CreateMenu() + if hSubMenu == 0 { + panic("failed CreateMenu") + } + return addMenuItem(m.hMenu, hSubMenu, text, Shortcut{}, nil, false) +} + +// This method will iterate through the menu items, group radio items together, build a +// quick access map and set the initial items +func updateRadioGroups() { + + if !initialised { + return + } + + radioItemsChecked := []*MenuItem{} + radioGroups = make(map[*MenuItem]*RadioGroup) + var currentRadioGroupMembers []*MenuItem + // Iterate the menus + for _, menu := range menuItems { + menuLength := len(menu) + for index, menuItem := range menu { + if menuItem.isRadio { + currentRadioGroupMembers = append(currentRadioGroupMembers, menuItem) + if menuItem.checked { + radioItemsChecked = append(radioItemsChecked, menuItem) + } + + // If end of menu + if index == menuLength-1 { + radioGroup := &RadioGroup{ + members: currentRadioGroupMembers, + hwnd: menuItem.hMenu, + } + // Save the group to each member iin the radiomap + for _, member := range currentRadioGroupMembers { + radioGroups[member] = radioGroup + } + currentRadioGroupMembers = []*MenuItem{} + } + continue + } + + // Not a radio item + if len(currentRadioGroupMembers) > 0 { + radioGroup := &RadioGroup{ + members: currentRadioGroupMembers, + hwnd: menuItem.hMenu, + } + // Save the group to each member iin the radiomap + for _, member := range currentRadioGroupMembers { + radioGroups[member] = radioGroup + } + currentRadioGroupMembers = []*MenuItem{} + } + } + } + + // Enable the checked items + for _, item := range radioItemsChecked { + radioGroup := radioGroups[item] + startID := radioGroup.members[0].id + endID := radioGroup.members[len(radioGroup.members)-1].id + w32.SelectRadioMenuItem(item.id, startID, endID, radioGroup.hwnd) + } + +} + +func (mi *MenuItem) OnClick() *EventManager { + return &mi.onClick +} + +func (mi *MenuItem) AddSeparator() { + addMenuItem(mi.hSubMenu, 0, "-", Shortcut{}, nil, false) +} + +// AddItem adds plain menu item. +func (mi *MenuItem) AddItem(text string, shortcut Shortcut) *MenuItem { + return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, false) +} + +// AddItemCheckable adds plain menu item that can have a checkmark. +func (mi *MenuItem) AddItemCheckable(text string, shortcut Shortcut) *MenuItem { + return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true) +} + +// AddItemRadio adds plain menu item that can have a checkmark and is part of a radio group. +func (mi *MenuItem) AddItemRadio(text string, shortcut Shortcut) *MenuItem { + menuItem := addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true) + menuItem.isRadio = true + return menuItem +} + +// AddItemWithBitmap adds menu item with shortcut and bitmap. +func (mi *MenuItem) AddItemWithBitmap(text string, shortcut Shortcut, image *Bitmap) *MenuItem { + return addMenuItem(mi.hSubMenu, 0, text, shortcut, image, false) +} + +// AddSubMenu adds a submenu. +func (mi *MenuItem) AddSubMenu(text string) *MenuItem { + hSubMenu := w32.CreatePopupMenu() + if hSubMenu == 0 { + panic("failed CreatePopupMenu") + } + return addMenuItem(mi.hSubMenu, hSubMenu, text, Shortcut{}, nil, false) +} + +// AddItem to the menu, set text to "-" for separators. +func addMenuItem(hMenu, hSubMenu w32.HMENU, text string, shortcut Shortcut, image *Bitmap, checkable bool) *MenuItem { + item := &MenuItem{ + hMenu: hMenu, + hSubMenu: hSubMenu, + text: text, + shortcut: shortcut, + image: image, + enabled: true, + id: nextMenuItemID, + checkable: checkable, + isRadio: false, + //visible: true, + } + nextMenuItemID++ + actionsByID[item.id] = item + menuItems[hMenu] = append(menuItems[hMenu], item) + + var mii w32.MENUITEMINFO + initMenuItemInfoFromAction(&mii, item) + + index := -1 + if !w32.InsertMenuItem(hMenu, uint32(index), true, &mii) { + panic("InsertMenuItem failed") + } + return item +} + +func indexInObserver(a *MenuItem) int { + var idx int + for _, mi := range menuItems[a.hMenu] { + if mi == a { + return idx + } + idx++ + } + return -1 +} + +func findMenuItemByID(id int) *MenuItem { + return actionsByID[uint16(id)] +} + +func (mi *MenuItem) update() { + var mii w32.MENUITEMINFO + initMenuItemInfoFromAction(&mii, mi) + + if !w32.SetMenuItemInfo(mi.hMenu, uint32(indexInObserver(mi)), true, &mii) { + panic("SetMenuItemInfo failed") + } + if mi.isRadio { + mi.updateRadioGroup() + } +} + +func (mi *MenuItem) IsSeparator() bool { return mi.text == "-" } +func (mi *MenuItem) SetSeparator() { mi.text = "-" } + +func (mi *MenuItem) Enabled() bool { return mi.enabled } +func (mi *MenuItem) SetEnabled(b bool) { mi.enabled = b; mi.update() } + +func (mi *MenuItem) Checkable() bool { return mi.checkable } +func (mi *MenuItem) SetCheckable(b bool) { mi.checkable = b; mi.update() } + +func (mi *MenuItem) Checked() bool { return mi.checked } +func (mi *MenuItem) SetChecked(b bool) { + if mi.isRadio { + radioGroup := radioGroups[mi] + if radioGroup != nil { + for _, member := range radioGroup.members { + member.checked = false + } + } + + } + mi.checked = b + mi.update() +} + +func (mi *MenuItem) Text() string { return mi.text } +func (mi *MenuItem) SetText(s string) { mi.text = s; mi.update() } + +func (mi *MenuItem) Image() *Bitmap { return mi.image } +func (mi *MenuItem) SetImage(b *Bitmap) { mi.image = b; mi.update() } + +func (mi *MenuItem) ToolTip() string { return mi.toolTip } +func (mi *MenuItem) SetToolTip(s string) { mi.toolTip = s; mi.update() } + +func (mi *MenuItem) updateRadioGroup() { + radioGroup := radioGroups[mi] + if radioGroup == nil { + return + } + startID := radioGroup.members[0].id + endID := radioGroup.members[len(radioGroup.members)-1].id + w32.SelectRadioMenuItem(mi.id, startID, endID, radioGroup.hwnd) +} diff --git a/v2/internal/frontend/desktop/windows/winc/mousecontrol.go b/v2/internal/frontend/desktop/windows/winc/mousecontrol.go new file mode 100644 index 000000000..2b89dd307 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/mousecontrol.go @@ -0,0 +1,50 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// MouseControl used for creating custom controls that need mouse hover or mouse leave events. +type MouseControl struct { + ControlBase + isMouseLeft bool +} + +func (cc *MouseControl) Init(parent Controller, className string, exStyle, style uint) { + RegClassOnlyOnce(className) + cc.hwnd = CreateWindow(className, parent, exStyle, style) + cc.parent = parent + RegMsgHandler(cc) + + cc.isMouseLeft = true + cc.SetFont(DefaultFont) +} + +func (cc *MouseControl) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + sender := GetMsgHandler(cc.hwnd) + switch msg { + case w32.WM_CREATE: + internalTrackMouseEvent(cc.hwnd) + cc.onCreate.Fire(NewEvent(sender, nil)) + case w32.WM_CLOSE: + cc.onClose.Fire(NewEvent(sender, nil)) + case w32.WM_MOUSEMOVE: + //if cc.isMouseLeft { + + cc.onMouseHover.Fire(NewEvent(sender, nil)) + //internalTrackMouseEvent(cc.hwnd) + cc.isMouseLeft = false + + //} + case w32.WM_MOUSELEAVE: + cc.onMouseLeave.Fire(NewEvent(sender, nil)) + cc.isMouseLeft = true + } + return w32.DefWindowProc(cc.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/msghandlerregistry.go b/v2/internal/frontend/desktop/windows/winc/msghandlerregistry.go new file mode 100644 index 000000000..2f165e3c5 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/msghandlerregistry.go @@ -0,0 +1,28 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func RegMsgHandler(controller Controller) { + gControllerRegistry[controller.Handle()] = controller +} + +func UnRegMsgHandler(hwnd w32.HWND) { + delete(gControllerRegistry, hwnd) +} + +func GetMsgHandler(hwnd w32.HWND) Controller { + if controller, isExists := gControllerRegistry[hwnd]; isExists { + return controller + } + + return nil +} diff --git a/v2/internal/frontend/desktop/windows/winc/panel.go b/v2/internal/frontend/desktop/windows/winc/panel.go new file mode 100644 index 000000000..f6aaea26b --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/panel.go @@ -0,0 +1,200 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Panel struct { + ControlBase + layoutMng LayoutManager +} + +func NewPanel(parent Controller) *Panel { + pa := new(Panel) + + RegClassOnlyOnce("winc_Panel") + pa.hwnd = CreateWindow("winc_Panel", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + pa.parent = parent + RegMsgHandler(pa) + + pa.SetFont(DefaultFont) + pa.SetText("") + pa.SetSize(200, 65) + return pa +} + +// SetLayout panel implements DockAllow interface. +func (pa *Panel) SetLayout(mng LayoutManager) { + pa.layoutMng = mng +} + +func (pa *Panel) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_SIZE, w32.WM_PAINT: + if pa.layoutMng != nil { + pa.layoutMng.Update() + } + } + return w32.DefWindowProc(pa.hwnd, msg, wparam, lparam) +} + +var errorPanelPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(255, 128, 128))) +var errorPanelOkPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(220, 220, 220))) + +// ErrorPanel shows errors or important messages. +// It is meant to stand out of other on screen controls. +type ErrorPanel struct { + ControlBase + pen *Pen + margin int +} + +// NewErrorPanel. +func NewErrorPanel(parent Controller) *ErrorPanel { + f := new(ErrorPanel) + f.init(parent) + + f.SetFont(DefaultFont) + f.SetText("No errors") + f.SetSize(200, 65) + f.margin = 5 + f.pen = errorPanelOkPen + return f +} + +func (epa *ErrorPanel) init(parent Controller) { + RegClassOnlyOnce("winc_ErrorPanel") + + epa.hwnd = CreateWindow("winc_ErrorPanel", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + epa.parent = parent + + RegMsgHandler(epa) +} + +func (epa *ErrorPanel) SetMargin(margin int) { + epa.margin = margin +} + +func (epa *ErrorPanel) Printf(format string, v ...interface{}) { + epa.SetText(fmt.Sprintf(format, v...)) + epa.ShowAsError(false) +} + +func (epa *ErrorPanel) Errorf(format string, v ...interface{}) { + epa.SetText(fmt.Sprintf(format, v...)) + epa.ShowAsError(true) +} + +func (epa *ErrorPanel) ShowAsError(show bool) { + if show { + epa.pen = errorPanelPen + } else { + epa.pen = errorPanelOkPen + } + epa.Invalidate(true) +} + +func (epa *ErrorPanel) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_ERASEBKGND: + canvas := NewCanvasFromHDC(w32.HDC(wparam)) + r := epa.Bounds() + r.rect.Left += int32(epa.margin) + r.rect.Right -= int32(epa.margin) + r.rect.Top += int32(epa.margin) + r.rect.Bottom -= int32(epa.margin) + // old code used NewSystemColorBrush(w32.COLOR_BTNFACE) + canvas.DrawFillRect(r, epa.pen, NewSystemColorBrush(w32.COLOR_WINDOW)) + + r.rect.Left += 5 + canvas.DrawText(epa.Text(), r, 0, epa.Font(), RGB(0, 0, 0)) + canvas.Dispose() + return 1 + } + return w32.DefWindowProc(epa.hwnd, msg, wparam, lparam) +} + +// MultiPanel contains other panels and only makes one of them visible. +type MultiPanel struct { + ControlBase + current int + panels []*Panel +} + +func NewMultiPanel(parent Controller) *MultiPanel { + mpa := new(MultiPanel) + + RegClassOnlyOnce("winc_MultiPanel") + mpa.hwnd = CreateWindow("winc_MultiPanel", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + mpa.parent = parent + RegMsgHandler(mpa) + + mpa.SetFont(DefaultFont) + mpa.SetText("") + mpa.SetSize(300, 200) + mpa.current = -1 + return mpa +} + +func (mpa *MultiPanel) Count() int { return len(mpa.panels) } + +// AddPanel adds panels to the internal list, first panel is visible all others are hidden. +func (mpa *MultiPanel) AddPanel(panel *Panel) { + if len(mpa.panels) > 0 { + panel.Hide() + } + mpa.current = 0 + mpa.panels = append(mpa.panels, panel) +} + +// ReplacePanel replaces panel, useful for refreshing controls on screen. +func (mpa *MultiPanel) ReplacePanel(index int, panel *Panel) { + mpa.panels[index] = panel +} + +// DeletePanel removed panel. +func (mpa *MultiPanel) DeletePanel(index int) { + mpa.panels = append(mpa.panels[:index], mpa.panels[index+1:]...) +} + +func (mpa *MultiPanel) Current() int { + return mpa.current +} + +func (mpa *MultiPanel) SetCurrent(index int) { + if index >= len(mpa.panels) { + panic("index greater than number of panels") + } + if mpa.current == -1 { + panic("no current panel, add panels first") + } + for i := range mpa.panels { + if i != index { + mpa.panels[i].Hide() + mpa.panels[i].Invalidate(true) + } + } + mpa.panels[index].Show() + mpa.panels[index].Invalidate(true) + mpa.current = index +} + +func (mpa *MultiPanel) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_SIZE: + // resize contained panels + for _, p := range mpa.panels { + p.SetPos(0, 0) + p.SetSize(mpa.Size()) + } + } + return w32.DefWindowProc(mpa.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/path.go b/v2/internal/frontend/desktop/windows/winc/path.go new file mode 100644 index 000000000..f52a2b931 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/path.go @@ -0,0 +1,77 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func knownFolderPath(id w32.CSIDL) (string, error) { + var buf [w32.MAX_PATH]uint16 + + if !w32.SHGetSpecialFolderPath(0, &buf[0], id, false) { + return "", fmt.Errorf("SHGetSpecialFolderPath failed") + } + + return syscall.UTF16ToString(buf[0:]), nil +} + +func AppDataPath() (string, error) { + return knownFolderPath(w32.CSIDL_APPDATA) +} + +func CommonAppDataPath() (string, error) { + return knownFolderPath(w32.CSIDL_COMMON_APPDATA) +} + +func LocalAppDataPath() (string, error) { + return knownFolderPath(w32.CSIDL_LOCAL_APPDATA) +} + +// EnsureAppDataPath uses AppDataPath to ensure storage for local settings and databases. +func EnsureAppDataPath(company, product string) (string, error) { + path, err := AppDataPath() + if err != nil { + return path, err + } + p := filepath.Join(path, company, product) + + if _, err := os.Stat(p); os.IsNotExist(err) { + // path/to/whatever does not exist + if err := os.MkdirAll(p, os.ModePerm); err != nil { + return p, err + } + } + return p, nil +} + +func DriveNames() ([]string, error) { + bufLen := w32.GetLogicalDriveStrings(0, nil) + if bufLen == 0 { + return nil, fmt.Errorf("GetLogicalDriveStrings failed") + } + buf := make([]uint16, bufLen+1) + + bufLen = w32.GetLogicalDriveStrings(bufLen+1, &buf[0]) + if bufLen == 0 { + return nil, fmt.Errorf("GetLogicalDriveStrings failed") + } + + var names []string + for i := 0; i < len(buf)-2; { + name := syscall.UTF16ToString(buf[i:]) + names = append(names, name) + i += len(name) + 1 + } + return names, nil +} diff --git a/v2/internal/frontend/desktop/windows/winc/pen.go b/v2/internal/frontend/desktop/windows/winc/pen.go new file mode 100644 index 000000000..232f2bb4f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/pen.go @@ -0,0 +1,61 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Pen struct { + hPen w32.HPEN + style uint + brush *Brush +} + +func NewPen(style uint, width uint, brush *Brush) *Pen { + if brush == nil { + panic("Brush cannot be nil") + } + + hPen := w32.ExtCreatePen(style, width, brush.GetLOGBRUSH(), 0, nil) + if hPen == 0 { + panic("Failed to create pen") + } + + return &Pen{hPen, style, brush} +} + +func NewNullPen() *Pen { + lb := w32.LOGBRUSH{LbStyle: w32.BS_NULL} + + hPen := w32.ExtCreatePen(w32.PS_COSMETIC|w32.PS_NULL, 1, &lb, 0, nil) + if hPen == 0 { + panic("failed to create null brush") + } + + return &Pen{hPen: hPen} +} + +func (pen *Pen) Style() uint { + return pen.style +} + +func (pen *Pen) Brush() *Brush { + return pen.brush +} + +func (pen *Pen) GetHPEN() w32.HPEN { + return pen.hPen +} + +func (pen *Pen) Dispose() { + if pen.hPen != 0 { + w32.DeleteObject(w32.HGDIOBJ(pen.hPen)) + pen.hPen = 0 + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/progressbar.go b/v2/internal/frontend/desktop/windows/winc/progressbar.go new file mode 100644 index 000000000..5d51a8a50 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/progressbar.go @@ -0,0 +1,48 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ProgressBar struct { + ControlBase +} + +func NewProgressBar(parent Controller) *ProgressBar { + pb := new(ProgressBar) + + pb.InitControl(w32.PROGRESS_CLASS, parent, 0, w32.WS_CHILD|w32.WS_VISIBLE) + RegMsgHandler(pb) + + pb.SetSize(200, 22) + return pb +} + +func (pr *ProgressBar) Value() int { + ret := w32.SendMessage(pr.hwnd, w32.PBM_GETPOS, 0, 0) + return int(ret) +} + +func (pr *ProgressBar) SetValue(v int) { + w32.SendMessage(pr.hwnd, w32.PBM_SETPOS, uintptr(v), 0) +} + +func (pr *ProgressBar) Range() (min, max uint) { + min = uint(w32.SendMessage(pr.hwnd, w32.PBM_GETRANGE, uintptr(w32.BoolToBOOL(true)), 0)) + max = uint(w32.SendMessage(pr.hwnd, w32.PBM_GETRANGE, uintptr(w32.BoolToBOOL(false)), 0)) + return +} + +func (pr *ProgressBar) SetRange(min, max int) { + w32.SendMessage(pr.hwnd, w32.PBM_SETRANGE32, uintptr(min), uintptr(max)) +} + +func (pr *ProgressBar) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + return w32.DefWindowProc(pr.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/rect.go b/v2/internal/frontend/desktop/windows/winc/rect.go new file mode 100644 index 000000000..dd9a70845 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/rect.go @@ -0,0 +1,87 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Rect struct { + rect w32.RECT +} + +func NewEmptyRect() *Rect { + var newRect Rect + w32.SetRectEmpty(&newRect.rect) + + return &newRect +} + +func NewRect(left, top, right, bottom int) *Rect { + var newRect Rect + w32.SetRectEmpty(&newRect.rect) + newRect.Set(left, top, right, bottom) + + return &newRect +} + +func (re *Rect) Data() (left, top, right, bottom int32) { + left = re.rect.Left + top = re.rect.Top + right = re.rect.Right + bottom = re.rect.Bottom + return +} + +func (re *Rect) Width() int { + return int(re.rect.Right - re.rect.Left) +} + +func (re *Rect) Height() int { + return int(re.rect.Bottom - re.rect.Top) +} + +func (re *Rect) GetW32Rect() *w32.RECT { + return &re.rect +} + +func (re *Rect) Set(left, top, right, bottom int) { + w32.SetRect(&re.rect, left, top, right, bottom) +} + +func (re *Rect) IsEqual(rect *Rect) bool { + return w32.EqualRect(&re.rect, &rect.rect) +} + +func (re *Rect) Inflate(x, y int) { + w32.InflateRect(&re.rect, x, y) +} + +func (re *Rect) Intersect(src *Rect) { + w32.IntersectRect(&re.rect, &re.rect, &src.rect) +} + +func (re *Rect) IsEmpty() bool { + return w32.IsRectEmpty(&re.rect) +} + +func (re *Rect) Offset(x, y int) { + w32.OffsetRect(&re.rect, x, y) +} + +func (re *Rect) IsPointIn(x, y int) bool { + return w32.PtInRect(&re.rect, x, y) +} + +func (re *Rect) Substract(src *Rect) { + w32.SubtractRect(&re.rect, &re.rect, &src.rect) +} + +func (re *Rect) Union(src *Rect) { + w32.UnionRect(&re.rect, &re.rect, &src.rect) +} diff --git a/v2/internal/frontend/desktop/windows/winc/resizer.go b/v2/internal/frontend/desktop/windows/winc/resizer.go new file mode 100644 index 000000000..2e9ea02f8 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/resizer.go @@ -0,0 +1,216 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type VResizer struct { + ControlBase + + control1 Dockable + control2 Dockable + dir Direction + + mouseLeft bool + drag bool +} + +func NewVResizer(parent Controller) *VResizer { + sp := new(VResizer) + + RegClassOnlyOnce("winc_VResizer") + sp.hwnd = CreateWindow("winc_VResizer", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + sp.parent = parent + sp.mouseLeft = true + RegMsgHandler(sp) + + sp.SetFont(DefaultFont) + sp.SetText("") + sp.SetSize(20, 100) + return sp +} + +func (sp *VResizer) SetControl(control1, control2 Dockable, dir Direction, minSize int) { + sp.control1 = control1 + sp.control2 = control2 + if dir != Left && dir != Right { + panic("invalid direction") + } + sp.dir = dir + + // TODO(vi): ADDED + /*internalTrackMouseEvent(control1.Handle()) + internalTrackMouseEvent(control2.Handle()) + + control1.OnMouseMove().Bind(func(e *Event) { + if sp.drag { + x := e.Data.(*MouseEventData).X + sp.update(x) + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_SIZEWE)) + + } + fmt.Println("control1.OnMouseMove") + }) + + control2.OnMouseMove().Bind(func(e *Event) { + if sp.drag { + x := e.Data.(*MouseEventData).X + sp.update(x) + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_SIZEWE)) + + } + fmt.Println("control2.OnMouseMove") + }) + + control1.OnLBUp().Bind(func(e *Event) { + sp.drag = false + sp.mouseLeft = true + fmt.Println("control1.OnLBUp") + }) + + control2.OnLBUp().Bind(func(e *Event) { + sp.drag = false + sp.mouseLeft = true + fmt.Println("control2.OnLBUp") + })*/ + + // ---- finish ADDED + +} + +func (sp *VResizer) update(x int) { + pos := x - 10 + + w1, h1 := sp.control1.Width(), sp.control1.Height() + if sp.dir == Left { + w1 += pos + } else { + w1 -= pos + } + sp.control1.SetSize(w1, h1) + fm := sp.parent.(*Form) + fm.UpdateLayout() + + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_ARROW)) +} + +func (sp *VResizer) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_CREATE: + internalTrackMouseEvent(sp.hwnd) + + case w32.WM_MOUSEMOVE: + if sp.drag { + x, _ := genPoint(lparam) + sp.update(x) + } else { + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_SIZEWE)) + } + + if sp.mouseLeft { + internalTrackMouseEvent(sp.hwnd) + sp.mouseLeft = false + } + + case w32.WM_MOUSELEAVE: + sp.drag = false + sp.mouseLeft = true + + case w32.WM_LBUTTONUP: + sp.drag = false + + case w32.WM_LBUTTONDOWN: + sp.drag = true + } + return w32.DefWindowProc(sp.hwnd, msg, wparam, lparam) +} + +type HResizer struct { + ControlBase + + control1 Dockable + control2 Dockable + dir Direction + mouseLeft bool + drag bool +} + +func NewHResizer(parent Controller) *HResizer { + sp := new(HResizer) + + RegClassOnlyOnce("winc_HResizer") + sp.hwnd = CreateWindow("winc_HResizer", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE) + sp.parent = parent + sp.mouseLeft = true + RegMsgHandler(sp) + + sp.SetFont(DefaultFont) + sp.SetText("") + sp.SetSize(100, 20) + + return sp +} + +func (sp *HResizer) SetControl(control1, control2 Dockable, dir Direction, minSize int) { + sp.control1 = control1 + sp.control2 = control2 + if dir != Top && dir != Bottom { + panic("invalid direction") + } + sp.dir = dir + +} + +func (sp *HResizer) update(y int) { + pos := y - 10 + + w1, h1 := sp.control1.Width(), sp.control1.Height() + if sp.dir == Top { + h1 += pos + } else { + h1 -= pos + } + sp.control1.SetSize(w1, h1) + + fm := sp.parent.(*Form) + fm.UpdateLayout() + + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_ARROW)) +} + +func (sp *HResizer) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_CREATE: + internalTrackMouseEvent(sp.hwnd) + + case w32.WM_MOUSEMOVE: + if sp.drag { + _, y := genPoint(lparam) + sp.update(y) + } else { + w32.SetCursor(w32.LoadCursorWithResourceID(0, w32.IDC_SIZENS)) + } + + if sp.mouseLeft { + internalTrackMouseEvent(sp.hwnd) + sp.mouseLeft = false + } + + case w32.WM_MOUSELEAVE: + sp.drag = false + sp.mouseLeft = true + + case w32.WM_LBUTTONUP: + sp.drag = false + + case w32.WM_LBUTTONDOWN: + sp.drag = true + } + return w32.DefWindowProc(sp.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/scrollview.go b/v2/internal/frontend/desktop/windows/winc/scrollview.go new file mode 100644 index 000000000..d9e932932 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/scrollview.go @@ -0,0 +1,121 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ScrollView struct { + ControlBase + child Dockable +} + +func NewScrollView(parent Controller) *ScrollView { + sv := new(ScrollView) + + RegClassOnlyOnce("winc_ScrollView") + sv.hwnd = CreateWindow("winc_ScrollView", parent, w32.WS_EX_CONTROLPARENT, + w32.WS_CHILD|w32.WS_HSCROLL|w32.WS_VISIBLE|w32.WS_VSCROLL) + sv.parent = parent + RegMsgHandler(sv) + + sv.SetFont(DefaultFont) + sv.SetText("") + sv.SetSize(200, 200) + return sv +} + +func (sv *ScrollView) SetChild(child Dockable) { + sv.child = child +} + +func (sv *ScrollView) UpdateScrollBars() { + w, h := sv.child.Width(), sv.child.Height() + sw, sh := sv.Size() + + var si w32.SCROLLINFO + si.CbSize = uint32(unsafe.Sizeof(si)) + si.FMask = w32.SIF_PAGE | w32.SIF_RANGE + + si.NMax = int32(w - 1) + si.NPage = uint32(sw) + w32.SetScrollInfo(sv.hwnd, w32.SB_HORZ, &si, true) + x := sv.scroll(w32.SB_HORZ, w32.SB_THUMBPOSITION) + + si.NMax = int32(h) + si.NPage = uint32(sh) + w32.SetScrollInfo(sv.hwnd, w32.SB_VERT, &si, true) + y := sv.scroll(w32.SB_VERT, w32.SB_THUMBPOSITION) + + sv.child.SetPos(x, y) +} + +func (sv *ScrollView) scroll(sb int32, cmd uint16) int { + var pos int32 + var si w32.SCROLLINFO + si.CbSize = uint32(unsafe.Sizeof(si)) + si.FMask = w32.SIF_PAGE | w32.SIF_POS | w32.SIF_RANGE | w32.SIF_TRACKPOS + + w32.GetScrollInfo(sv.hwnd, sb, &si) + pos = si.NPos + + switch cmd { + case w32.SB_LINELEFT: // == win.SB_LINEUP + pos -= 20 + + case w32.SB_LINERIGHT: // == win.SB_LINEDOWN + pos += 20 + + case w32.SB_PAGELEFT: // == win.SB_PAGEUP + pos -= int32(si.NPage) + + case w32.SB_PAGERIGHT: // == win.SB_PAGEDOWN + pos += int32(si.NPage) + + case w32.SB_THUMBTRACK: + pos = si.NTrackPos + } + + if pos < 0 { + pos = 0 + } + if pos > si.NMax+1-int32(si.NPage) { + pos = si.NMax + 1 - int32(si.NPage) + } + + si.FMask = w32.SIF_POS + si.NPos = pos + w32.SetScrollInfo(sv.hwnd, sb, &si, true) + + return -int(pos) +} + +func (sv *ScrollView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + if sv.child != nil { + switch msg { + case w32.WM_PAINT: + sv.UpdateScrollBars() + + case w32.WM_HSCROLL: + x, y := sv.child.Pos() + x = sv.scroll(w32.SB_HORZ, w32.LOWORD(uint32(wparam))) + sv.child.SetPos(x, y) + + case w32.WM_VSCROLL: + x, y := sv.child.Pos() + y = sv.scroll(w32.SB_VERT, w32.LOWORD(uint32(wparam))) + sv.child.SetPos(x, y) + + case w32.WM_SIZE, w32.WM_SIZING: + sv.UpdateScrollBars() + } + } + return w32.DefWindowProc(sv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/slider.go b/v2/internal/frontend/desktop/windows/winc/slider.go new file mode 100644 index 000000000..2be9f13b6 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/slider.go @@ -0,0 +1,79 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + +type Slider struct { + ControlBase + prevPos int + + onScroll EventManager +} + +func NewSlider(parent Controller) *Slider { + tb := new(Slider) + + tb.InitControl("msctls_trackbar32", parent, 0, w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD /*|w32.TBS_AUTOTICKS*/) + RegMsgHandler(tb) + + tb.SetFont(DefaultFont) + tb.SetText("Slider") + tb.SetSize(200, 32) + + tb.SetRange(0, 100) + tb.SetPage(10) + return tb +} + +func (tb *Slider) OnScroll() *EventManager { + return &tb.onScroll +} + +func (tb *Slider) Value() int { + ret := w32.SendMessage(tb.hwnd, w32.TBM_GETPOS, 0, 0) + return int(ret) +} + +func (tb *Slider) SetValue(v int) { + tb.prevPos = v + w32.SendMessage(tb.hwnd, w32.TBM_SETPOS, uintptr(w32.BoolToBOOL(true)), uintptr(v)) +} + +func (tb *Slider) Range() (min, max int) { + min = int(w32.SendMessage(tb.hwnd, w32.TBM_GETRANGEMIN, 0, 0)) + max = int(w32.SendMessage(tb.hwnd, w32.TBM_GETRANGEMAX, 0, 0)) + return min, max +} + +func (tb *Slider) SetRange(min, max int) { + w32.SendMessage(tb.hwnd, w32.TBM_SETRANGE, uintptr(w32.BoolToBOOL(true)), uintptr(w32.MAKELONG(uint16(min), uint16(max)))) +} + +func (tb *Slider) SetPage(pagesize int) { + w32.SendMessage(tb.hwnd, w32.TBM_SETPAGESIZE, 0, uintptr(pagesize)) +} + +func (tb *Slider) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + /* + // REMOVE: + // following code did not work, used workaround below + code := w32.LOWORD(uint32(wparam)) + + switch code { + case w32.TB_ENDTRACK: + tb.onScroll.Fire(NewEvent(tb, nil)) + }*/ + + newPos := tb.Value() + if newPos != tb.prevPos { + tb.onScroll.Fire(NewEvent(tb, nil)) + tb.prevPos = newPos + } + + return w32.DefWindowProc(tb.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/tabview.go b/v2/internal/frontend/desktop/windows/winc/tabview.go new file mode 100644 index 000000000..5e8fe5093 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/tabview.go @@ -0,0 +1,107 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// TabView creates MultiPanel internally and manages tabs as panels. +type TabView struct { + ControlBase + + panels *MultiPanel + onSelectedChange EventManager +} + +func NewTabView(parent Controller) *TabView { + tv := new(TabView) + + tv.InitControl("SysTabControl32", parent, 0, + w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.WS_CLIPSIBLINGS) + RegMsgHandler(tv) + + tv.panels = NewMultiPanel(parent) + + tv.SetFont(DefaultFont) + tv.SetSize(200, 24) + return tv +} + +func (tv *TabView) Panels() *MultiPanel { + return tv.panels +} + +func (tv *TabView) tcitemFromPage(panel *Panel) *w32.TCITEM { + text := syscall.StringToUTF16(panel.Text()) + item := &w32.TCITEM{ + Mask: w32.TCIF_TEXT, + PszText: &text[0], + CchTextMax: int32(len(text)), + } + return item +} + +func (tv *TabView) AddPanel(text string) *Panel { + panel := NewPanel(tv.panels) + panel.SetText(text) + + item := tv.tcitemFromPage(panel) + index := tv.panels.Count() + idx := int(w32.SendMessage(tv.hwnd, w32.TCM_INSERTITEM, uintptr(index), uintptr(unsafe.Pointer(item)))) + if idx == -1 { + panic("SendMessage(TCM_INSERTITEM) failed") + } + + tv.panels.AddPanel(panel) + tv.SetCurrent(idx) + return panel +} + +func (tv *TabView) DeletePanel(index int) { + w32.SendMessage(tv.hwnd, w32.TCM_DELETEITEM, uintptr(index), 0) + tv.panels.DeletePanel(index) + switch { + case tv.panels.Count() > index: + tv.SetCurrent(index) + case tv.panels.Count() == 0: + tv.SetCurrent(0) + } +} + +func (tv *TabView) Current() int { + return tv.panels.Current() +} + +func (tv *TabView) SetCurrent(index int) { + if index < 0 || index >= tv.panels.Count() { + panic("invalid index") + } + if ret := int(w32.SendMessage(tv.hwnd, w32.TCM_SETCURSEL, uintptr(index), 0)); ret == -1 { + panic("SendMessage(TCM_SETCURSEL) failed") + } + tv.panels.SetCurrent(index) +} + +func (tv *TabView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_NOTIFY: + nmhdr := (*w32.NMHDR)(unsafe.Pointer(lparam)) + + switch int32(nmhdr.Code) { + case w32.TCN_SELCHANGE: + cur := int(w32.SendMessage(tv.hwnd, w32.TCM_GETCURSEL, 0, 0)) + tv.SetCurrent(cur) + + tv.onSelectedChange.Fire(NewEvent(tv, nil)) + } + } + return w32.DefWindowProc(tv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/toolbar.go b/v2/internal/frontend/desktop/windows/winc/toolbar.go new file mode 100644 index 000000000..bbe945e1c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/toolbar.go @@ -0,0 +1,181 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type Toolbar struct { + ControlBase + iml *ImageList + + buttons []*ToolButton +} + +type ToolButton struct { + tb *Toolbar + + text string + enabled bool + checkable bool + checked bool + image int + + onClick EventManager +} + +func (bt *ToolButton) OnClick() *EventManager { + return &bt.onClick +} + +func (bt *ToolButton) update() { bt.tb.update(bt) } + +func (bt *ToolButton) IsSeparator() bool { return bt.text == "-" } +func (bt *ToolButton) SetSeparator() { bt.text = "-" } + +func (bt *ToolButton) Enabled() bool { return bt.enabled } +func (bt *ToolButton) SetEnabled(b bool) { bt.enabled = b; bt.update() } + +func (bt *ToolButton) Checkable() bool { return bt.checkable } +func (bt *ToolButton) SetCheckable(b bool) { bt.checkable = b; bt.update() } + +func (bt *ToolButton) Checked() bool { return bt.checked } +func (bt *ToolButton) SetChecked(b bool) { bt.checked = b; bt.update() } + +func (bt *ToolButton) Text() string { return bt.text } +func (bt *ToolButton) SetText(s string) { bt.text = s; bt.update() } + +func (bt *ToolButton) Image() int { return bt.image } +func (bt *ToolButton) SetImage(i int) { bt.image = i; bt.update() } + +// NewHToolbar creates horizontal toolbar with text on same line as image. +func NewHToolbar(parent Controller) *Toolbar { + return newToolbar(parent, w32.CCS_NODIVIDER|w32.TBSTYLE_FLAT|w32.TBSTYLE_TOOLTIPS|w32.TBSTYLE_WRAPABLE| + w32.WS_CHILD|w32.TBSTYLE_LIST) +} + +// NewToolbar creates toolbar with text below the image. +func NewToolbar(parent Controller) *Toolbar { + return newToolbar(parent, w32.CCS_NODIVIDER|w32.TBSTYLE_FLAT|w32.TBSTYLE_TOOLTIPS|w32.TBSTYLE_WRAPABLE| + w32.WS_CHILD /*|w32.TBSTYLE_TRANSPARENT*/) +} + +func newToolbar(parent Controller, style uint) *Toolbar { + tb := new(Toolbar) + + tb.InitControl("ToolbarWindow32", parent, 0, style) + + exStyle := w32.SendMessage(tb.hwnd, w32.TB_GETEXTENDEDSTYLE, 0, 0) + exStyle |= w32.TBSTYLE_EX_DRAWDDARROWS | w32.TBSTYLE_EX_MIXEDBUTTONS + w32.SendMessage(tb.hwnd, w32.TB_SETEXTENDEDSTYLE, 0, exStyle) + RegMsgHandler(tb) + + tb.SetFont(DefaultFont) + tb.SetPos(0, 0) + tb.SetSize(200, 40) + + return tb +} + +func (tb *Toolbar) SetImageList(imageList *ImageList) { + w32.SendMessage(tb.hwnd, w32.TB_SETIMAGELIST, 0, uintptr(imageList.Handle())) + tb.iml = imageList +} + +func (tb *Toolbar) initButton(btn *ToolButton, state, style *byte, image *int32, text *uintptr) { + *style |= w32.BTNS_AUTOSIZE + + if btn.checked { + *state |= w32.TBSTATE_CHECKED + } + + if btn.enabled { + *state |= w32.TBSTATE_ENABLED + } + + if btn.checkable { + *style |= w32.BTNS_CHECK + } + + if len(btn.Text()) > 0 { + *style |= w32.BTNS_SHOWTEXT + } + + if btn.IsSeparator() { + *style = w32.BTNS_SEP + } + + *image = int32(btn.Image()) + *text = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(btn.Text()))) +} + +func (tb *Toolbar) update(btn *ToolButton) { + tbbi := w32.TBBUTTONINFO{ + DwMask: w32.TBIF_IMAGE | w32.TBIF_STATE | w32.TBIF_STYLE | w32.TBIF_TEXT, + } + + tbbi.CbSize = uint32(unsafe.Sizeof(tbbi)) + + var i int + for i = range tb.buttons { + if tb.buttons[i] == btn { + break + } + } + + tb.initButton(btn, &tbbi.FsState, &tbbi.FsStyle, &tbbi.IImage, &tbbi.PszText) + if w32.SendMessage(tb.hwnd, w32.TB_SETBUTTONINFO, uintptr(i), uintptr(unsafe.Pointer(&tbbi))) == 0 { + panic("SendMessage(TB_SETBUTTONINFO) failed") + } +} + +func (tb *Toolbar) AddSeparator() { + tb.AddButton("-", 0) +} + +// AddButton creates and adds button to the toolbar. Use returned toolbutton to setup OnClick event. +func (tb *Toolbar) AddButton(text string, image int) *ToolButton { + bt := &ToolButton{ + tb: tb, // points to parent + text: text, + image: image, + enabled: true, + } + tb.buttons = append(tb.buttons, bt) + index := len(tb.buttons) - 1 + + tbb := w32.TBBUTTON{ + IdCommand: int32(index), + } + + tb.initButton(bt, &tbb.FsState, &tbb.FsStyle, &tbb.IBitmap, &tbb.IString) + w32.SendMessage(tb.hwnd, w32.TB_BUTTONSTRUCTSIZE, uintptr(unsafe.Sizeof(tbb)), 0) + + if w32.SendMessage(tb.hwnd, w32.TB_INSERTBUTTON, uintptr(index), uintptr(unsafe.Pointer(&tbb))) == w32.FALSE { + panic("SendMessage(TB_ADDBUTTONS)") + } + + w32.SendMessage(tb.hwnd, w32.TB_AUTOSIZE, 0, 0) + return bt +} + +func (tb *Toolbar) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_COMMAND: + switch w32.HIWORD(uint32(wparam)) { + case w32.BN_CLICKED: + id := uint16(w32.LOWORD(uint32(wparam))) + btn := tb.buttons[id] + btn.onClick.Fire(NewEvent(tb, nil)) + } + } + return w32.DefWindowProc(tb.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/tooltip.go b/v2/internal/frontend/desktop/windows/winc/tooltip.go new file mode 100644 index 000000000..ec1568bb9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/tooltip.go @@ -0,0 +1,45 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +type ToolTip struct { + ControlBase +} + +func NewToolTip(parent Controller) *ToolTip { + tp := new(ToolTip) + + tp.InitControl("tooltips_class32", parent, w32.WS_EX_TOPMOST, w32.WS_POPUP|w32.TTS_NOPREFIX|w32.TTS_ALWAYSTIP) + w32.SetWindowPos(tp.Handle(), w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOACTIVATE) + + return tp +} + +func (tp *ToolTip) SetTip(tool Controller, tip string) bool { + var ti w32.TOOLINFO + ti.CbSize = uint32(unsafe.Sizeof(ti)) + if tool.Parent() != nil { + ti.Hwnd = tool.Parent().Handle() + } + ti.UFlags = w32.TTF_IDISHWND | w32.TTF_SUBCLASS /* | TTF_ABSOLUTE */ + ti.UId = uintptr(tool.Handle()) + ti.LpszText = syscall.StringToUTF16Ptr(tip) + + return w32.SendMessage(tp.Handle(), w32.TTM_ADDTOOL, 0, uintptr(unsafe.Pointer(&ti))) != w32.FALSE +} + +func (tp *ToolTip) WndProc(msg uint, wparam, lparam uintptr) uintptr { + return w32.DefWindowProc(tp.hwnd, uint32(msg), wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/treeview.go b/v2/internal/frontend/desktop/windows/winc/treeview.go new file mode 100644 index 000000000..2cdc0e936 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/treeview.go @@ -0,0 +1,286 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + */ + +package winc + +import ( + "errors" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +// TreeItem represents an item in a TreeView widget. +type TreeItem interface { + Text() string // Text returns the text of the item. + ImageIndex() int // ImageIndex is used only if SetImageList is called on the treeview +} + +type treeViewItemInfo struct { + handle w32.HTREEITEM + child2Handle map[TreeItem]w32.HTREEITEM +} + +// StringTreeItem is helper for basic string lists. +type StringTreeItem struct { + Data string + Image int +} + +func (s StringTreeItem) Text() string { return s.Data } +func (s StringTreeItem) ImageIndex() int { return s.Image } + +type TreeView struct { + ControlBase + + iml *ImageList + item2Info map[TreeItem]*treeViewItemInfo + handle2Item map[w32.HTREEITEM]TreeItem + currItem TreeItem + + onSelectedChange EventManager + onExpand EventManager + onCollapse EventManager + onViewChange EventManager +} + +func NewTreeView(parent Controller) *TreeView { + tv := new(TreeView) + + tv.InitControl("SysTreeView32", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE| + w32.WS_BORDER|w32.TVS_HASBUTTONS|w32.TVS_LINESATROOT|w32.TVS_SHOWSELALWAYS| + w32.TVS_TRACKSELECT /*|w32.WS_EX_CLIENTEDGE*/) + + tv.item2Info = make(map[TreeItem]*treeViewItemInfo) + tv.handle2Item = make(map[w32.HTREEITEM]TreeItem) + + RegMsgHandler(tv) + + tv.SetFont(DefaultFont) + tv.SetSize(200, 400) + + if err := tv.SetTheme("Explorer"); err != nil { + // theme error is ignored + } + return tv +} + +func (tv *TreeView) EnableDoubleBuffer(enable bool) { + if enable { + w32.SendMessage(tv.hwnd, w32.TVM_SETEXTENDEDSTYLE, 0, w32.TVS_EX_DOUBLEBUFFER) + } else { + w32.SendMessage(tv.hwnd, w32.TVM_SETEXTENDEDSTYLE, w32.TVS_EX_DOUBLEBUFFER, 0) + } +} + +// SelectedItem is current selected item after OnSelectedChange event. +func (tv *TreeView) SelectedItem() TreeItem { + return tv.currItem +} + +func (tv *TreeView) SetSelectedItem(item TreeItem) bool { + var handle w32.HTREEITEM + if item != nil { + if info := tv.item2Info[item]; info == nil { + return false // invalid item + } else { + handle = info.handle + } + } + + if w32.SendMessage(tv.hwnd, w32.TVM_SELECTITEM, w32.TVGN_CARET, uintptr(handle)) == 0 { + return false // set selected failed + } + tv.currItem = item + return true +} + +func (tv *TreeView) ItemAt(x, y int) TreeItem { + hti := w32.TVHITTESTINFO{Pt: w32.POINT{int32(x), int32(y)}} + w32.SendMessage(tv.hwnd, w32.TVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) + if item, ok := tv.handle2Item[hti.HItem]; ok { + return item + } + return nil +} + +func (tv *TreeView) Items() (list []TreeItem) { + for item := range tv.item2Info { + list = append(list, item) + } + return list +} + +func (tv *TreeView) InsertItem(item, parent, insertAfter TreeItem) error { + var tvins w32.TVINSERTSTRUCT + tvi := &tvins.Item + + tvi.Mask = w32.TVIF_TEXT // w32.TVIF_CHILDREN | w32.TVIF_TEXT + tvi.PszText = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(item.Text()))) // w32.LPSTR_TEXTCALLBACK + tvi.CChildren = 0 // w32.I_CHILDRENCALLBACK + + if parent == nil { + tvins.HParent = w32.TVI_ROOT + } else { + info := tv.item2Info[parent] + if info == nil { + return errors.New("winc: invalid parent") + } + tvins.HParent = info.handle + } + + if insertAfter == nil { + tvins.HInsertAfter = w32.TVI_LAST + } else { + info := tv.item2Info[insertAfter] + if info == nil { + return errors.New("winc: invalid prev item") + } + tvins.HInsertAfter = info.handle + } + + tv.applyImage(tvi, item) + + hItem := w32.HTREEITEM(w32.SendMessage(tv.hwnd, w32.TVM_INSERTITEM, 0, uintptr(unsafe.Pointer(&tvins)))) + if hItem == 0 { + return errors.New("winc: TVM_INSERTITEM failed") + } + tv.item2Info[item] = &treeViewItemInfo{hItem, make(map[TreeItem]w32.HTREEITEM)} + tv.handle2Item[hItem] = item + return nil +} + +func (tv *TreeView) UpdateItem(item TreeItem) bool { + it := tv.item2Info[item] + if it == nil { + return false + } + + tvi := &w32.TVITEM{ + Mask: w32.TVIF_TEXT, + HItem: it.handle, + PszText: uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(item.Text()))), + } + tv.applyImage(tvi, item) + + if w32.SendMessage(tv.hwnd, w32.TVM_SETITEM, 0, uintptr(unsafe.Pointer(tvi))) == 0 { + return false + } + return true +} + +func (tv *TreeView) DeleteItem(item TreeItem) bool { + it := tv.item2Info[item] + if it == nil { + return false + } + + if w32.SendMessage(tv.hwnd, w32.TVM_DELETEITEM, 0, uintptr(it.handle)) == 0 { + return false + } + + delete(tv.item2Info, item) + delete(tv.handle2Item, it.handle) + return true +} + +func (tv *TreeView) DeleteAllItems() bool { + if w32.SendMessage(tv.hwnd, w32.TVM_DELETEITEM, 0, 0) == 0 { + return false + } + + tv.item2Info = make(map[TreeItem]*treeViewItemInfo) + tv.handle2Item = make(map[w32.HTREEITEM]TreeItem) + return true +} + +func (tv *TreeView) Expand(item TreeItem) bool { + if w32.SendMessage(tv.hwnd, w32.TVM_EXPAND, w32.TVE_EXPAND, uintptr(tv.item2Info[item].handle)) == 0 { + return false + } + return true +} + +func (tv *TreeView) Collapse(item TreeItem) bool { + if w32.SendMessage(tv.hwnd, w32.TVM_EXPAND, w32.TVE_COLLAPSE, uintptr(tv.item2Info[item].handle)) == 0 { + return false + } + return true +} + +func (tv *TreeView) EnsureVisible(item TreeItem) bool { + if info := tv.item2Info[item]; info != nil { + return w32.SendMessage(tv.hwnd, w32.TVM_ENSUREVISIBLE, 0, uintptr(info.handle)) != 0 + } + return false +} + +func (tv *TreeView) SetImageList(imageList *ImageList) { + w32.SendMessage(tv.hwnd, w32.TVM_SETIMAGELIST, 0, uintptr(imageList.Handle())) + tv.iml = imageList +} + +func (tv *TreeView) applyImage(tc *w32.TVITEM, item TreeItem) { + if tv.iml != nil { + tc.Mask |= w32.TVIF_IMAGE | w32.TVIF_SELECTEDIMAGE + tc.IImage = int32(item.ImageIndex()) + tc.ISelectedImage = int32(item.ImageIndex()) + } +} + +func (tv *TreeView) OnSelectedChange() *EventManager { + return &tv.onSelectedChange +} + +func (tv *TreeView) OnExpand() *EventManager { + return &tv.onExpand +} + +func (tv *TreeView) OnCollapse() *EventManager { + return &tv.onCollapse +} + +func (tv *TreeView) OnViewChange() *EventManager { + return &tv.onViewChange +} + +// Message processor +func (tv *TreeView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_NOTIFY: + nm := (*w32.NMHDR)(unsafe.Pointer(lparam)) + + switch nm.Code { + case w32.TVN_ITEMEXPANDED: + nmtv := (*w32.NMTREEVIEW)(unsafe.Pointer(lparam)) + + switch nmtv.Action { + case w32.TVE_COLLAPSE: + tv.onCollapse.Fire(NewEvent(tv, nil)) + + case w32.TVE_COLLAPSERESET: + + case w32.TVE_EXPAND: + tv.onExpand.Fire(NewEvent(tv, nil)) + + case w32.TVE_EXPANDPARTIAL: + + case w32.TVE_TOGGLE: + } + + case w32.TVN_SELCHANGED: + nmtv := (*w32.NMTREEVIEW)(unsafe.Pointer(lparam)) + tv.currItem = tv.handle2Item[nmtv.ItemNew.HItem] + tv.onSelectedChange.Fire(NewEvent(tv, nil)) + + case w32.TVN_GETDISPINFO: + tv.onViewChange.Fire(NewEvent(tv, nil)) + } + + } + return w32.DefWindowProc(tv.hwnd, msg, wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/winc/utils.go b/v2/internal/frontend/desktop/windows/winc/utils.go new file mode 100644 index 000000000..c2d91a8c3 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/utils.go @@ -0,0 +1,156 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +func internalTrackMouseEvent(hwnd w32.HWND) { + var tme w32.TRACKMOUSEEVENT + tme.CbSize = uint32(unsafe.Sizeof(tme)) + tme.DwFlags = w32.TME_LEAVE + tme.HwndTrack = hwnd + tme.DwHoverTime = w32.HOVER_DEFAULT + + w32.TrackMouseEvent(&tme) +} + +func SetStyle(hwnd w32.HWND, b bool, style int) { + originalStyle := int(w32.GetWindowLongPtr(hwnd, w32.GWL_STYLE)) + if originalStyle != 0 { + if b { + originalStyle |= style + } else { + originalStyle &^= style + } + w32.SetWindowLongPtr(hwnd, w32.GWL_STYLE, uintptr(originalStyle)) + } +} + +func SetExStyle(hwnd w32.HWND, b bool, style int) { + originalStyle := int(w32.GetWindowLongPtr(hwnd, w32.GWL_EXSTYLE)) + if originalStyle != 0 { + if b { + originalStyle |= style + } else { + originalStyle &^= style + } + w32.SetWindowLongPtr(hwnd, w32.GWL_EXSTYLE, uintptr(originalStyle)) + } +} + +func CreateWindow(className string, parent Controller, exStyle, style uint) w32.HWND { + instance := GetAppInstance() + var parentHwnd w32.HWND + if parent != nil { + parentHwnd = parent.Handle() + } + var hwnd w32.HWND + hwnd = w32.CreateWindowEx( + exStyle, + syscall.StringToUTF16Ptr(className), + nil, + style, + w32.CW_USEDEFAULT, + w32.CW_USEDEFAULT, + w32.CW_USEDEFAULT, + w32.CW_USEDEFAULT, + parentHwnd, + 0, + instance, + nil) + + if hwnd == 0 { + errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style) + panic(errStr) + } + + return hwnd +} + +func RegisterClass(className string, wndproc uintptr) { + instance := GetAppInstance() + icon := w32.LoadIconWithResourceID(instance, w32.IDI_APPLICATION) + + var wc w32.WNDCLASSEX + wc.Size = uint32(unsafe.Sizeof(wc)) + wc.Style = w32.CS_HREDRAW | w32.CS_VREDRAW + wc.WndProc = wndproc + wc.Instance = instance + wc.Background = w32.COLOR_BTNFACE + 1 + wc.Icon = icon + wc.Cursor = w32.LoadCursorWithResourceID(0, w32.IDC_ARROW) + wc.ClassName = syscall.StringToUTF16Ptr(className) + wc.MenuName = nil + wc.IconSm = icon + + if ret := w32.RegisterClassEx(&wc); ret == 0 { + panic(syscall.GetLastError()) + } +} + +func RegisterWindowMessage(name string) uint32 { + n := syscall.StringToUTF16Ptr(name) + + ret := w32.RegisterWindowMessage(n) + if ret == 0 { + panic(syscall.GetLastError()) + } + return ret +} + +func getMonitorInfo(hwnd w32.HWND) *w32.MONITORINFO { + currentMonitor := w32.MonitorFromWindow(hwnd, w32.MONITOR_DEFAULTTONEAREST) + var info w32.MONITORINFO + info.CbSize = uint32(unsafe.Sizeof(info)) + w32.GetMonitorInfo(currentMonitor, &info) + return &info +} +func getWindowInfo(hwnd w32.HWND) *w32.WINDOWINFO { + var info w32.WINDOWINFO + info.CbSize = uint32(unsafe.Sizeof(info)) + w32.GetWindowInfo(hwnd, &info) + return &info +} + +func RegClassOnlyOnce(className string) { + isExists := false + for _, class := range gRegisteredClasses { + if class == className { + isExists = true + break + } + } + + if !isExists { + RegisterClass(className, GeneralWndprocCallBack) + gRegisteredClasses = append(gRegisteredClasses, className) + } +} + +func ScreenToClientRect(hwnd w32.HWND, rect *w32.RECT) *Rect { + l, t, r, b := rect.Left, rect.Top, rect.Right, rect.Bottom + l1, t1, _ := w32.ScreenToClient(hwnd, int(l), int(t)) + r1, b1, _ := w32.ScreenToClient(hwnd, int(r), int(b)) + return NewRect(l1, t1, r1, b1) +} + +// ScaleWithDPI scales the pixels from the default DPI-Space (96) to the target DPI-Space. +func ScaleWithDPI(pixels int, dpi uint) int { + return (pixels * int(dpi)) / 96 +} + +// ScaleToDefaultDPI scales the pixels from scaled DPI-Space to default DPI-Space (96). +func ScaleToDefaultDPI(pixels int, dpi uint) int { + return (pixels * 96) / int(dpi) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/comctl32.go b/v2/internal/frontend/desktop/windows/winc/w32/comctl32.go new file mode 100644 index 000000000..b66709f5f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/comctl32.go @@ -0,0 +1,112 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modcomctl32 = syscall.NewLazyDLL("comctl32.dll") + + procInitCommonControlsEx = modcomctl32.NewProc("InitCommonControlsEx") + procImageList_Create = modcomctl32.NewProc("ImageList_Create") + procImageList_Destroy = modcomctl32.NewProc("ImageList_Destroy") + procImageList_GetImageCount = modcomctl32.NewProc("ImageList_GetImageCount") + procImageList_SetImageCount = modcomctl32.NewProc("ImageList_SetImageCount") + procImageList_Add = modcomctl32.NewProc("ImageList_Add") + procImageList_ReplaceIcon = modcomctl32.NewProc("ImageList_ReplaceIcon") + procImageList_Remove = modcomctl32.NewProc("ImageList_Remove") + procTrackMouseEvent = modcomctl32.NewProc("_TrackMouseEvent") +) + +func InitCommonControlsEx(lpInitCtrls *INITCOMMONCONTROLSEX) bool { + ret, _, _ := procInitCommonControlsEx.Call( + uintptr(unsafe.Pointer(lpInitCtrls))) + + return ret != 0 +} + +func ImageList_Create(cx, cy int, flags uint, cInitial, cGrow int) HIMAGELIST { + ret, _, _ := procImageList_Create.Call( + uintptr(cx), + uintptr(cy), + uintptr(flags), + uintptr(cInitial), + uintptr(cGrow)) + + if ret == 0 { + panic("Create image list failed") + } + + return HIMAGELIST(ret) +} + +func ImageList_Destroy(himl HIMAGELIST) bool { + ret, _, _ := procImageList_Destroy.Call( + uintptr(himl)) + + return ret != 0 +} + +func ImageList_GetImageCount(himl HIMAGELIST) int { + ret, _, _ := procImageList_GetImageCount.Call( + uintptr(himl)) + + return int(ret) +} + +func ImageList_SetImageCount(himl HIMAGELIST, uNewCount uint) bool { + ret, _, _ := procImageList_SetImageCount.Call( + uintptr(himl), + uintptr(uNewCount)) + + return ret != 0 +} + +func ImageList_Add(himl HIMAGELIST, hbmImage, hbmMask HBITMAP) int { + ret, _, _ := procImageList_Add.Call( + uintptr(himl), + uintptr(hbmImage), + uintptr(hbmMask)) + + return int(ret) +} + +func ImageList_ReplaceIcon(himl HIMAGELIST, i int, hicon HICON) int { + ret, _, _ := procImageList_ReplaceIcon.Call( + uintptr(himl), + uintptr(i), + uintptr(hicon)) + + return int(ret) +} + +func ImageList_AddIcon(himl HIMAGELIST, hicon HICON) int { + return ImageList_ReplaceIcon(himl, -1, hicon) +} + +func ImageList_Remove(himl HIMAGELIST, i int) bool { + ret, _, _ := procImageList_Remove.Call( + uintptr(himl), + uintptr(i)) + + return ret != 0 +} + +func ImageList_RemoveAll(himl HIMAGELIST) bool { + return ImageList_Remove(himl, -1) +} + +func TrackMouseEvent(tme *TRACKMOUSEEVENT) bool { + ret, _, _ := procTrackMouseEvent.Call( + uintptr(unsafe.Pointer(tme))) + + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/comdlg32.go b/v2/internal/frontend/desktop/windows/winc/w32/comdlg32.go new file mode 100644 index 000000000..d28922c33 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/comdlg32.go @@ -0,0 +1,40 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modcomdlg32 = syscall.NewLazyDLL("comdlg32.dll") + + procGetSaveFileName = modcomdlg32.NewProc("GetSaveFileNameW") + procGetOpenFileName = modcomdlg32.NewProc("GetOpenFileNameW") + procCommDlgExtendedError = modcomdlg32.NewProc("CommDlgExtendedError") +) + +func GetOpenFileName(ofn *OPENFILENAME) bool { + ret, _, _ := procGetOpenFileName.Call( + uintptr(unsafe.Pointer(ofn))) + + return ret != 0 +} + +func GetSaveFileName(ofn *OPENFILENAME) bool { + ret, _, _ := procGetSaveFileName.Call( + uintptr(unsafe.Pointer(ofn))) + + return ret != 0 +} + +func CommDlgExtendedError() uint { + ret, _, _ := procCommDlgExtendedError.Call() + + return uint(ret) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/constants.go b/v2/internal/frontend/desktop/windows/winc/w32/constants.go new file mode 100644 index 000000000..49b507a23 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/constants.go @@ -0,0 +1,3526 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +const ( + FALSE = 0 + TRUE = 1 +) + +const ( + NO_ERROR = 0 + ERROR_SUCCESS = 0 + ERROR_FILE_NOT_FOUND = 2 + ERROR_PATH_NOT_FOUND = 3 + ERROR_ACCESS_DENIED = 5 + ERROR_INVALID_HANDLE = 6 + ERROR_BAD_FORMAT = 11 + ERROR_INVALID_NAME = 123 + ERROR_MORE_DATA = 234 + ERROR_NO_MORE_ITEMS = 259 + ERROR_INVALID_SERVICE_CONTROL = 1052 + ERROR_SERVICE_REQUEST_TIMEOUT = 1053 + ERROR_SERVICE_NO_THREAD = 1054 + ERROR_SERVICE_DATABASE_LOCKED = 1055 + ERROR_SERVICE_ALREADY_RUNNING = 1056 + ERROR_SERVICE_DISABLED = 1058 + ERROR_SERVICE_DOES_NOT_EXIST = 1060 + ERROR_SERVICE_CANNOT_ACCEPT_CTRL = 1061 + ERROR_SERVICE_NOT_ACTIVE = 1062 + ERROR_DATABASE_DOES_NOT_EXIST = 1065 + ERROR_SERVICE_DEPENDENCY_FAIL = 1068 + ERROR_SERVICE_LOGON_FAILED = 1069 + ERROR_SERVICE_MARKED_FOR_DELETE = 1072 + ERROR_SERVICE_DEPENDENCY_DELETED = 1075 +) + +const ( + SE_ERR_FNF = 2 + SE_ERR_PNF = 3 + SE_ERR_ACCESSDENIED = 5 + SE_ERR_OOM = 8 + SE_ERR_DLLNOTFOUND = 32 + SE_ERR_SHARE = 26 + SE_ERR_ASSOCINCOMPLETE = 27 + SE_ERR_DDETIMEOUT = 28 + SE_ERR_DDEFAIL = 29 + SE_ERR_DDEBUSY = 30 + SE_ERR_NOASSOC = 31 +) + +const ( + CW_USEDEFAULT = ^0x7fffffff +) + +const ( + IMAGE_BITMAP = 0 + IMAGE_ICON = 1 + IMAGE_CURSOR = 2 + IMAGE_ENHMETAFILE = 3 +) + +// ShowWindow constants +const ( + SW_HIDE = 0 + SW_NORMAL = 1 + SW_SHOWNORMAL = 1 + SW_SHOWMINIMIZED = 2 + SW_MAXIMIZE = 3 + SW_SHOWMAXIMIZED = 3 + SW_SHOWNOACTIVATE = 4 + SW_SHOW = 5 + SW_MINIMIZE = 6 + SW_SHOWMINNOACTIVE = 7 + SW_SHOWNA = 8 + SW_RESTORE = 9 + SW_SHOWDEFAULT = 10 + SW_FORCEMINIMIZE = 11 +) + +// Window class styles +const ( + CS_VREDRAW = 0x00000001 + CS_HREDRAW = 0x00000002 + CS_KEYCVTWINDOW = 0x00000004 + CS_DBLCLKS = 0x00000008 + CS_OWNDC = 0x00000020 + CS_CLASSDC = 0x00000040 + CS_PARENTDC = 0x00000080 + CS_NOKEYCVT = 0x00000100 + CS_NOCLOSE = 0x00000200 + CS_SAVEBITS = 0x00000800 + CS_BYTEALIGNCLIENT = 0x00001000 + CS_BYTEALIGNWINDOW = 0x00002000 + CS_GLOBALCLASS = 0x00004000 + CS_IME = 0x00010000 + CS_DROPSHADOW = 0x00020000 +) + +// Predefined cursor constants +const ( + IDC_ARROW = 32512 + IDC_IBEAM = 32513 + IDC_WAIT = 32514 + IDC_CROSS = 32515 + IDC_UPARROW = 32516 + IDC_SIZENWSE = 32642 + IDC_SIZENESW = 32643 + IDC_SIZEWE = 32644 + IDC_SIZENS = 32645 + IDC_SIZEALL = 32646 + IDC_NO = 32648 + IDC_HAND = 32649 + IDC_APPSTARTING = 32650 + IDC_HELP = 32651 + IDC_ICON = 32641 + IDC_SIZE = 32640 +) + +// Predefined icon constants +const ( + IDI_APPLICATION = 32512 + IDI_HAND = 32513 + IDI_QUESTION = 32514 + IDI_EXCLAMATION = 32515 + IDI_ASTERISK = 32516 + IDI_WINLOGO = 32517 + IDI_WARNING = IDI_EXCLAMATION + IDI_ERROR = IDI_HAND + IDI_INFORMATION = IDI_ASTERISK +) + +// Button style constants +const ( + BS_3STATE = 5 + BS_AUTO3STATE = 6 + BS_AUTOCHECKBOX = 3 + BS_AUTORADIOBUTTON = 9 + BS_BITMAP = 128 + BS_BOTTOM = 0x800 + BS_CENTER = 0x300 + BS_CHECKBOX = 2 + BS_DEFPUSHBUTTON = 1 + BS_GROUPBOX = 7 + BS_ICON = 64 + BS_LEFT = 256 + BS_LEFTTEXT = 32 + BS_MULTILINE = 0x2000 + BS_NOTIFY = 0x4000 + BS_OWNERDRAW = 0xB + BS_PUSHBUTTON = 0 + BS_PUSHLIKE = 4096 + BS_RADIOBUTTON = 4 + BS_RIGHT = 512 + BS_RIGHTBUTTON = 32 + BS_TEXT = 0 + BS_TOP = 0x400 + BS_USERBUTTON = 8 + BS_VCENTER = 0xC00 + BS_FLAT = 0x8000 + BS_SPLITBUTTON = 0x000C // >= Vista + BS_DEFSPLITBUTTON = 0x000D // >= Vista +) + +// Button state constants +const ( + BST_CHECKED = 1 + BST_INDETERMINATE = 2 + BST_UNCHECKED = 0 + BST_FOCUS = 8 + BST_PUSHED = 4 +) + +// Predefined brushes constants +const ( + COLOR_3DDKSHADOW = 21 + COLOR_3DFACE = 15 + COLOR_3DHILIGHT = 20 + COLOR_3DHIGHLIGHT = 20 + COLOR_3DLIGHT = 22 + COLOR_BTNHILIGHT = 20 + COLOR_3DSHADOW = 16 + COLOR_ACTIVEBORDER = 10 + COLOR_ACTIVECAPTION = 2 + COLOR_APPWORKSPACE = 12 + COLOR_BACKGROUND = 1 + COLOR_DESKTOP = 1 + COLOR_BTNFACE = 15 + COLOR_BTNHIGHLIGHT = 20 + COLOR_BTNSHADOW = 16 + COLOR_BTNTEXT = 18 + COLOR_CAPTIONTEXT = 9 + COLOR_GRAYTEXT = 17 + COLOR_HIGHLIGHT = 13 + COLOR_HIGHLIGHTTEXT = 14 + COLOR_INACTIVEBORDER = 11 + COLOR_INACTIVECAPTION = 3 + COLOR_INACTIVECAPTIONTEXT = 19 + COLOR_INFOBK = 24 + COLOR_INFOTEXT = 23 + COLOR_MENU = 4 + COLOR_MENUTEXT = 7 + COLOR_SCROLLBAR = 0 + COLOR_WINDOW = 5 + COLOR_WINDOWFRAME = 6 + COLOR_WINDOWTEXT = 8 + COLOR_HOTLIGHT = 26 + COLOR_GRADIENTACTIVECAPTION = 27 + COLOR_GRADIENTINACTIVECAPTION = 28 +) + +// Button message constants +const ( + BM_CLICK = 245 + BM_GETCHECK = 240 + BM_GETIMAGE = 246 + BM_GETSTATE = 242 + BM_SETCHECK = 241 + BM_SETIMAGE = 247 + BM_SETSTATE = 243 + BM_SETSTYLE = 244 +) + +// Button notifications +const ( + BN_CLICKED = 0 + BN_PAINT = 1 + BN_HILITE = 2 + BN_PUSHED = BN_HILITE + BN_UNHILITE = 3 + BN_UNPUSHED = BN_UNHILITE + BN_DISABLE = 4 + BN_DOUBLECLICKED = 5 + BN_DBLCLK = BN_DOUBLECLICKED + BN_SETFOCUS = 6 + BN_KILLFOCUS = 7 +) + +// TrackPopupMenu[Ex] flags +const ( + TPM_CENTERALIGN = 0x0004 + TPM_LEFTALIGN = 0x0000 + TPM_RIGHTALIGN = 0x0008 + TPM_BOTTOMALIGN = 0x0020 + TPM_TOPALIGN = 0x0000 + TPM_VCENTERALIGN = 0x0010 + TPM_NONOTIFY = 0x0080 + TPM_RETURNCMD = 0x0100 + TPM_LEFTBUTTON = 0x0000 + TPM_RIGHTBUTTON = 0x0002 + TPM_HORNEGANIMATION = 0x0800 + TPM_HORPOSANIMATION = 0x0400 + TPM_NOANIMATION = 0x4000 + TPM_VERNEGANIMATION = 0x2000 + TPM_VERPOSANIMATION = 0x1000 + TPM_HORIZONTAL = 0x0000 + TPM_VERTICAL = 0x0040 +) + +// GetWindowLong and GetWindowLongPtr constants +const ( + GWL_EXSTYLE = -20 + GWL_STYLE = -16 + GWL_WNDPROC = -4 + GWLP_WNDPROC = -4 + GWL_HINSTANCE = -6 + GWLP_HINSTANCE = -6 + GWL_HWNDPARENT = -8 + GWLP_HWNDPARENT = -8 + GWL_ID = -12 + GWLP_ID = -12 + GWL_USERDATA = -21 + GWLP_USERDATA = -21 +) + +// Window style constants +const ( + WS_OVERLAPPED = 0x00000000 + WS_POPUP = 0x80000000 + WS_CHILD = 0x40000000 + WS_MINIMIZE = 0x20000000 + WS_VISIBLE = 0x10000000 + WS_DISABLED = 0x08000000 + WS_CLIPSIBLINGS = 0x04000000 + WS_CLIPCHILDREN = 0x02000000 + WS_MAXIMIZE = 0x01000000 + WS_CAPTION = 0x00C00000 + WS_BORDER = 0x00800000 + WS_DLGFRAME = 0x00400000 + WS_VSCROLL = 0x00200000 + WS_HSCROLL = 0x00100000 + WS_SYSMENU = 0x00080000 + WS_THICKFRAME = 0x00040000 + WS_GROUP = 0x00020000 + WS_TABSTOP = 0x00010000 + WS_MINIMIZEBOX = 0x00020000 + WS_MAXIMIZEBOX = 0x00010000 + WS_TILED = 0x00000000 + WS_ICONIC = 0x20000000 + WS_SIZEBOX = 0x00040000 + WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000 + WS_POPUPWINDOW = 0x80000000 | 0x00800000 | 0x00080000 + WS_CHILDWINDOW = 0x40000000 +) + +// Extended window style constants +const ( + WS_EX_DLGMODALFRAME = 0x00000001 + WS_EX_NOPARENTNOTIFY = 0x00000004 + WS_EX_TOPMOST = 0x00000008 + WS_EX_ACCEPTFILES = 0x00000010 + WS_EX_TRANSPARENT = 0x00000020 + WS_EX_MDICHILD = 0x00000040 + WS_EX_TOOLWINDOW = 0x00000080 + WS_EX_WINDOWEDGE = 0x00000100 + WS_EX_CLIENTEDGE = 0x00000200 + WS_EX_CONTEXTHELP = 0x00000400 + WS_EX_RIGHT = 0x00001000 + WS_EX_LEFT = 0x00000000 + WS_EX_RTLREADING = 0x00002000 + WS_EX_LTRREADING = 0x00000000 + WS_EX_LEFTSCROLLBAR = 0x00004000 + WS_EX_RIGHTSCROLLBAR = 0x00000000 + WS_EX_CONTROLPARENT = 0x00010000 + WS_EX_STATICEDGE = 0x00020000 + WS_EX_APPWINDOW = 0x00040000 + WS_EX_OVERLAPPEDWINDOW = 0x00000100 | 0x00000200 + WS_EX_PALETTEWINDOW = 0x00000100 | 0x00000080 | 0x00000008 + WS_EX_LAYERED = 0x00080000 + WS_EX_NOINHERITLAYOUT = 0x00100000 + WS_EX_NOREDIRECTIONBITMAP = 0x00200000 + WS_EX_LAYOUTRTL = 0x00400000 + WS_EX_NOACTIVATE = 0x08000000 +) + +// Window message constants +const ( + WM_APP = 32768 + WM_ACTIVATE = 6 + WM_ACTIVATEAPP = 28 + WM_AFXFIRST = 864 + WM_AFXLAST = 895 + WM_ASKCBFORMATNAME = 780 + WM_CANCELJOURNAL = 75 + WM_CANCELMODE = 31 + WM_CAPTURECHANGED = 533 + WM_CHANGECBCHAIN = 781 + WM_CHAR = 258 + WM_CHARTOITEM = 47 + WM_CHILDACTIVATE = 34 + WM_CLEAR = 771 + WM_CLOSE = 16 + WM_COMMAND = 273 + WM_COMMNOTIFY = 68 /* OBSOLETE */ + WM_COMPACTING = 65 + WM_COMPAREITEM = 57 + WM_CONTEXTMENU = 123 + WM_COPY = 769 + WM_COPYDATA = 74 + WM_CREATE = 1 + WM_CTLCOLORBTN = 309 + WM_CTLCOLORDLG = 310 + WM_CTLCOLOREDIT = 307 + WM_CTLCOLORLISTBOX = 308 + WM_CTLCOLORMSGBOX = 306 + WM_CTLCOLORSCROLLBAR = 311 + WM_CTLCOLORSTATIC = 312 + WM_CUT = 768 + WM_DEADCHAR = 259 + WM_DELETEITEM = 45 + WM_DESTROY = 2 + WM_DESTROYCLIPBOARD = 775 + WM_DEVICECHANGE = 537 + WM_DEVMODECHANGE = 27 + WM_DISPLAYCHANGE = 126 + WM_DRAWCLIPBOARD = 776 + WM_DRAWITEM = 43 + WM_DROPFILES = 563 + WM_ENABLE = 10 + WM_ENDSESSION = 22 + WM_ENTERIDLE = 289 + WM_ENTERMENULOOP = 529 + WM_ENTERSIZEMOVE = 561 + WM_ERASEBKGND = 20 + WM_EXITMENULOOP = 530 + WM_EXITSIZEMOVE = 562 + WM_FONTCHANGE = 29 + WM_GETDLGCODE = 135 + WM_GETFONT = 49 + WM_GETHOTKEY = 51 + WM_GETICON = 127 + WM_GETMINMAXINFO = 36 + WM_GETTEXT = 13 + WM_GETTEXTLENGTH = 14 + WM_HANDHELDFIRST = 856 + WM_HANDHELDLAST = 863 + WM_HELP = 83 + WM_HOTKEY = 786 + WM_HSCROLL = 276 + WM_HSCROLLCLIPBOARD = 782 + WM_ICONERASEBKGND = 39 + WM_INITDIALOG = 272 + WM_INITMENU = 278 + WM_INITMENUPOPUP = 279 + WM_INPUT = 0x00FF + WM_INPUTLANGCHANGE = 81 + WM_INPUTLANGCHANGEREQUEST = 80 + WM_KEYDOWN = 256 + WM_KEYUP = 257 + WM_KILLFOCUS = 8 + WM_MDIACTIVATE = 546 + WM_MDICASCADE = 551 + WM_MDICREATE = 544 + WM_MDIDESTROY = 545 + WM_MDIGETACTIVE = 553 + WM_MDIICONARRANGE = 552 + WM_MDIMAXIMIZE = 549 + WM_MDINEXT = 548 + WM_MDIREFRESHMENU = 564 + WM_MDIRESTORE = 547 + WM_MDISETMENU = 560 + WM_MDITILE = 550 + WM_MEASUREITEM = 44 + WM_GETOBJECT = 0x003D + WM_CHANGEUISTATE = 0x0127 + WM_UPDATEUISTATE = 0x0128 + WM_QUERYUISTATE = 0x0129 + WM_UNINITMENUPOPUP = 0x0125 + WM_MENURBUTTONUP = 290 + WM_MENUCOMMAND = 0x0126 + WM_MENUGETOBJECT = 0x0124 + WM_MENUDRAG = 0x0123 + WM_APPCOMMAND = 0x0319 + WM_MENUCHAR = 288 + WM_MENUSELECT = 287 + WM_MOVE = 3 + WM_MOVING = 534 + WM_NCACTIVATE = 134 + WM_NCCALCSIZE = 131 + WM_NCCREATE = 129 + WM_NCDESTROY = 130 + WM_NCHITTEST = 132 + WM_NCLBUTTONDBLCLK = 163 + WM_NCLBUTTONDOWN = 161 + WM_NCLBUTTONUP = 162 + WM_NCMBUTTONDBLCLK = 169 + WM_NCMBUTTONDOWN = 167 + WM_NCMBUTTONUP = 168 + WM_NCXBUTTONDOWN = 171 + WM_NCXBUTTONUP = 172 + WM_NCXBUTTONDBLCLK = 173 + WM_NCMOUSEHOVER = 0x02A0 + WM_NCMOUSELEAVE = 0x02A2 + WM_NCMOUSEMOVE = 160 + WM_NCPAINT = 133 + WM_NCRBUTTONDBLCLK = 166 + WM_NCRBUTTONDOWN = 164 + WM_NCRBUTTONUP = 165 + WM_NEXTDLGCTL = 40 + WM_NEXTMENU = 531 + WM_NOTIFY = 78 + WM_NOTIFYFORMAT = 85 + WM_NULL = 0 + WM_PAINT = 15 + WM_PAINTCLIPBOARD = 777 + WM_PAINTICON = 38 + WM_PALETTECHANGED = 785 + WM_PALETTEISCHANGING = 784 + WM_PARENTNOTIFY = 528 + WM_PASTE = 770 + WM_PENWINFIRST = 896 + WM_PENWINLAST = 911 + WM_POWER = 72 + WM_POWERBROADCAST = 536 + WM_PRINT = 791 + WM_PRINTCLIENT = 792 + WM_QUERYDRAGICON = 55 + WM_QUERYENDSESSION = 17 + WM_QUERYNEWPALETTE = 783 + WM_QUERYOPEN = 19 + WM_QUEUESYNC = 35 + WM_QUIT = 18 + WM_RENDERALLFORMATS = 774 + WM_RENDERFORMAT = 773 + WM_SETCURSOR = 32 + WM_SETFOCUS = 7 + WM_SETFONT = 48 + WM_SETHOTKEY = 50 + WM_SETICON = 128 + WM_SETREDRAW = 11 + WM_SETTEXT = 12 + WM_SETTINGCHANGE = 26 + WM_SHOWWINDOW = 24 + WM_SIZE = 5 + WM_SIZECLIPBOARD = 779 + WM_SIZING = 532 + WM_SPOOLERSTATUS = 42 + WM_STYLECHANGED = 125 + WM_STYLECHANGING = 124 + WM_SYSCHAR = 262 + WM_SYSCOLORCHANGE = 21 + WM_SYSCOMMAND = 274 + WM_SYSDEADCHAR = 263 + WM_SYSKEYDOWN = 260 + WM_SYSKEYUP = 261 + WM_TCARD = 82 + WM_THEMECHANGED = 794 + WM_TIMECHANGE = 30 + WM_TIMER = 275 + WM_UNDO = 772 + WM_USER = 1024 + WM_USERCHANGED = 84 + WM_VKEYTOITEM = 46 + WM_VSCROLL = 277 + WM_VSCROLLCLIPBOARD = 778 + WM_WINDOWPOSCHANGED = 71 + WM_WINDOWPOSCHANGING = 70 + WM_WININICHANGE = 26 + WM_KEYFIRST = 256 + WM_KEYLAST = 264 + WM_SYNCPAINT = 136 + WM_MOUSEACTIVATE = 33 + WM_MOUSEMOVE = 512 + WM_LBUTTONDOWN = 513 + WM_LBUTTONUP = 514 + WM_LBUTTONDBLCLK = 515 + WM_RBUTTONDOWN = 516 + WM_RBUTTONUP = 517 + WM_RBUTTONDBLCLK = 518 + WM_MBUTTONDOWN = 519 + WM_MBUTTONUP = 520 + WM_MBUTTONDBLCLK = 521 + WM_MOUSEWHEEL = 522 + WM_MOUSEFIRST = 512 + WM_XBUTTONDOWN = 523 + WM_XBUTTONUP = 524 + WM_XBUTTONDBLCLK = 525 + WM_MOUSELAST = 525 + WM_MOUSEHOVER = 0x2A1 + WM_MOUSELEAVE = 0x2A3 + WM_CLIPBOARDUPDATE = 0x031D +) + +// WM_ACTIVATE +const ( + WA_INACTIVE = 0 + WA_ACTIVE = 1 + WA_CLICKACTIVE = 2 +) + +const LF_FACESIZE = 32 + +// Font weight constants +const ( + FW_DONTCARE = 0 + FW_THIN = 100 + FW_EXTRALIGHT = 200 + FW_ULTRALIGHT = FW_EXTRALIGHT + FW_LIGHT = 300 + FW_NORMAL = 400 + FW_REGULAR = 400 + FW_MEDIUM = 500 + FW_SEMIBOLD = 600 + FW_DEMIBOLD = FW_SEMIBOLD + FW_BOLD = 700 + FW_EXTRABOLD = 800 + FW_ULTRABOLD = FW_EXTRABOLD + FW_HEAVY = 900 + FW_BLACK = FW_HEAVY +) + +// Charset constants +const ( + ANSI_CHARSET = 0 + DEFAULT_CHARSET = 1 + SYMBOL_CHARSET = 2 + SHIFTJIS_CHARSET = 128 + HANGEUL_CHARSET = 129 + HANGUL_CHARSET = 129 + GB2312_CHARSET = 134 + CHINESEBIG5_CHARSET = 136 + GREEK_CHARSET = 161 + TURKISH_CHARSET = 162 + HEBREW_CHARSET = 177 + ARABIC_CHARSET = 178 + BALTIC_CHARSET = 186 + RUSSIAN_CHARSET = 204 + THAI_CHARSET = 222 + EASTEUROPE_CHARSET = 238 + OEM_CHARSET = 255 + JOHAB_CHARSET = 130 + VIETNAMESE_CHARSET = 163 + MAC_CHARSET = 77 +) + +// Font output precision constants +const ( + OUT_DEFAULT_PRECIS = 0 + OUT_STRING_PRECIS = 1 + OUT_CHARACTER_PRECIS = 2 + OUT_STROKE_PRECIS = 3 + OUT_TT_PRECIS = 4 + OUT_DEVICE_PRECIS = 5 + OUT_RASTER_PRECIS = 6 + OUT_TT_ONLY_PRECIS = 7 + OUT_OUTLINE_PRECIS = 8 + OUT_PS_ONLY_PRECIS = 10 +) + +// Font clipping precision constants +const ( + CLIP_DEFAULT_PRECIS = 0 + CLIP_CHARACTER_PRECIS = 1 + CLIP_STROKE_PRECIS = 2 + CLIP_MASK = 15 + CLIP_LH_ANGLES = 16 + CLIP_TT_ALWAYS = 32 + CLIP_EMBEDDED = 128 +) + +// Font output quality constants +const ( + DEFAULT_QUALITY = 0 + DRAFT_QUALITY = 1 + PROOF_QUALITY = 2 + NONANTIALIASED_QUALITY = 3 + ANTIALIASED_QUALITY = 4 + CLEARTYPE_QUALITY = 5 +) + +// Font pitch constants +const ( + DEFAULT_PITCH = 0 + FIXED_PITCH = 1 + VARIABLE_PITCH = 2 +) + +// Font family constants +const ( + FF_DECORATIVE = 80 + FF_DONTCARE = 0 + FF_MODERN = 48 + FF_ROMAN = 16 + FF_SCRIPT = 64 + FF_SWISS = 32 +) + +// DeviceCapabilities capabilities +const ( + DC_FIELDS = 1 + DC_PAPERS = 2 + DC_PAPERSIZE = 3 + DC_MINEXTENT = 4 + DC_MAXEXTENT = 5 + DC_BINS = 6 + DC_DUPLEX = 7 + DC_SIZE = 8 + DC_EXTRA = 9 + DC_VERSION = 10 + DC_DRIVER = 11 + DC_BINNAMES = 12 + DC_ENUMRESOLUTIONS = 13 + DC_FILEDEPENDENCIES = 14 + DC_TRUETYPE = 15 + DC_PAPERNAMES = 16 + DC_ORIENTATION = 17 + DC_COPIES = 18 + DC_BINADJUST = 19 + DC_EMF_COMPLIANT = 20 + DC_DATATYPE_PRODUCED = 21 + DC_COLLATE = 22 + DC_MANUFACTURER = 23 + DC_MODEL = 24 + DC_PERSONALITY = 25 + DC_PRINTRATE = 26 + DC_PRINTRATEUNIT = 27 + DC_PRINTERMEM = 28 + DC_MEDIAREADY = 29 + DC_STAPLE = 30 + DC_PRINTRATEPPM = 31 + DC_COLORDEVICE = 32 + DC_NUP = 33 + DC_MEDIATYPENAMES = 34 + DC_MEDIATYPES = 35 +) + +// GetDeviceCaps index constants +const ( + DRIVERVERSION = 0 + TECHNOLOGY = 2 + HORZSIZE = 4 + VERTSIZE = 6 + HORZRES = 8 + VERTRES = 10 + LOGPIXELSX = 88 + LOGPIXELSY = 90 + BITSPIXEL = 12 + PLANES = 14 + NUMBRUSHES = 16 + NUMPENS = 18 + NUMFONTS = 22 + NUMCOLORS = 24 + NUMMARKERS = 20 + ASPECTX = 40 + ASPECTY = 42 + ASPECTXY = 44 + PDEVICESIZE = 26 + CLIPCAPS = 36 + SIZEPALETTE = 104 + NUMRESERVED = 106 + COLORRES = 108 + PHYSICALWIDTH = 110 + PHYSICALHEIGHT = 111 + PHYSICALOFFSETX = 112 + PHYSICALOFFSETY = 113 + SCALINGFACTORX = 114 + SCALINGFACTORY = 115 + VREFRESH = 116 + DESKTOPHORZRES = 118 + DESKTOPVERTRES = 117 + BLTALIGNMENT = 119 + SHADEBLENDCAPS = 120 + COLORMGMTCAPS = 121 + RASTERCAPS = 38 + CURVECAPS = 28 + LINECAPS = 30 + POLYGONALCAPS = 32 + TEXTCAPS = 34 +) + +// GetDeviceCaps TECHNOLOGY constants +const ( + DT_PLOTTER = 0 + DT_RASDISPLAY = 1 + DT_RASPRINTER = 2 + DT_RASCAMERA = 3 + DT_CHARSTREAM = 4 + DT_METAFILE = 5 + DT_DISPFILE = 6 +) + +// GetDeviceCaps SHADEBLENDCAPS constants +const ( + SB_NONE = 0x00 + SB_CONST_ALPHA = 0x01 + SB_PIXEL_ALPHA = 0x02 + SB_PREMULT_ALPHA = 0x04 + SB_GRAD_RECT = 0x10 + SB_GRAD_TRI = 0x20 +) + +// GetDeviceCaps COLORMGMTCAPS constants +const ( + CM_NONE = 0x00 + CM_DEVICE_ICM = 0x01 + CM_GAMMA_RAMP = 0x02 + CM_CMYK_COLOR = 0x04 +) + +// GetDeviceCaps RASTERCAPS constants +const ( + RC_BANDING = 2 + RC_BITBLT = 1 + RC_BITMAP64 = 8 + RC_DI_BITMAP = 128 + RC_DIBTODEV = 512 + RC_FLOODFILL = 4096 + RC_GDI20_OUTPUT = 16 + RC_PALETTE = 256 + RC_SCALING = 4 + RC_STRETCHBLT = 2048 + RC_STRETCHDIB = 8192 + RC_DEVBITS = 0x8000 + RC_OP_DX_OUTPUT = 0x4000 +) + +// GetDeviceCaps CURVECAPS constants +const ( + CC_NONE = 0 + CC_CIRCLES = 1 + CC_PIE = 2 + CC_CHORD = 4 + CC_ELLIPSES = 8 + CC_WIDE = 16 + CC_STYLED = 32 + CC_WIDESTYLED = 64 + CC_INTERIORS = 128 + CC_ROUNDRECT = 256 +) + +// GetDeviceCaps LINECAPS constants +const ( + LC_NONE = 0 + LC_POLYLINE = 2 + LC_MARKER = 4 + LC_POLYMARKER = 8 + LC_WIDE = 16 + LC_STYLED = 32 + LC_WIDESTYLED = 64 + LC_INTERIORS = 128 +) + +// GetDeviceCaps POLYGONALCAPS constants +const ( + PC_NONE = 0 + PC_POLYGON = 1 + PC_POLYPOLYGON = 256 + PC_PATHS = 512 + PC_RECTANGLE = 2 + PC_WINDPOLYGON = 4 + PC_SCANLINE = 8 + PC_TRAPEZOID = 4 + PC_WIDE = 16 + PC_STYLED = 32 + PC_WIDESTYLED = 64 + PC_INTERIORS = 128 +) + +// GetDeviceCaps TEXTCAPS constants +const ( + TC_OP_CHARACTER = 1 + TC_OP_STROKE = 2 + TC_CP_STROKE = 4 + TC_CR_90 = 8 + TC_CR_ANY = 16 + TC_SF_X_YINDEP = 32 + TC_SA_DOUBLE = 64 + TC_SA_INTEGER = 128 + TC_SA_CONTIN = 256 + TC_EA_DOUBLE = 512 + TC_IA_ABLE = 1024 + TC_UA_ABLE = 2048 + TC_SO_ABLE = 4096 + TC_RA_ABLE = 8192 + TC_VA_ABLE = 16384 + TC_RESERVED = 32768 + TC_SCROLLBLT = 65536 +) + +// Static control styles +const ( + SS_BITMAP = 14 + SS_BLACKFRAME = 7 + SS_BLACKRECT = 4 + SS_CENTER = 1 + SS_CENTERIMAGE = 512 + SS_EDITCONTROL = 0x2000 + SS_ENHMETAFILE = 15 + SS_ETCHEDFRAME = 18 + SS_ETCHEDHORZ = 16 + SS_ETCHEDVERT = 17 + SS_GRAYFRAME = 8 + SS_GRAYRECT = 5 + SS_ICON = 3 + SS_LEFT = 0 + SS_LEFTNOWORDWRAP = 0xc + SS_NOPREFIX = 128 + SS_NOTIFY = 256 + SS_OWNERDRAW = 0xd + SS_REALSIZECONTROL = 0x040 + SS_REALSIZEIMAGE = 0x800 + SS_RIGHT = 2 + SS_RIGHTJUST = 0x400 + SS_SIMPLE = 11 + SS_SUNKEN = 4096 + SS_WHITEFRAME = 9 + SS_WHITERECT = 6 + SS_USERITEM = 10 + SS_TYPEMASK = 0x0000001F + SS_ENDELLIPSIS = 0x00004000 + SS_PATHELLIPSIS = 0x00008000 + SS_WORDELLIPSIS = 0x0000C000 + SS_ELLIPSISMASK = 0x0000C000 +) + +// Edit styles +const ( + ES_LEFT = 0x0000 + ES_CENTER = 0x0001 + ES_RIGHT = 0x0002 + ES_MULTILINE = 0x0004 + ES_UPPERCASE = 0x0008 + ES_LOWERCASE = 0x0010 + ES_PASSWORD = 0x0020 + ES_AUTOVSCROLL = 0x0040 + ES_AUTOHSCROLL = 0x0080 + ES_NOHIDESEL = 0x0100 + ES_OEMCONVERT = 0x0400 + ES_READONLY = 0x0800 + ES_WANTRETURN = 0x1000 + ES_NUMBER = 0x2000 +) + +// Edit notifications +const ( + EN_SETFOCUS = 0x0100 + EN_KILLFOCUS = 0x0200 + EN_CHANGE = 0x0300 + EN_UPDATE = 0x0400 + EN_ERRSPACE = 0x0500 + EN_MAXTEXT = 0x0501 + EN_HSCROLL = 0x0601 + EN_VSCROLL = 0x0602 + EN_ALIGN_LTR_EC = 0x0700 + EN_ALIGN_RTL_EC = 0x0701 +) + +// Edit messages +const ( + EM_GETSEL = 0x00B0 + EM_SETSEL = 0x00B1 + EM_GETRECT = 0x00B2 + EM_SETRECT = 0x00B3 + EM_SETRECTNP = 0x00B4 + EM_SCROLL = 0x00B5 + EM_LINESCROLL = 0x00B6 + EM_SCROLLCARET = 0x00B7 + EM_GETMODIFY = 0x00B8 + EM_SETMODIFY = 0x00B9 + EM_GETLINECOUNT = 0x00BA + EM_LINEINDEX = 0x00BB + EM_SETHANDLE = 0x00BC + EM_GETHANDLE = 0x00BD + EM_GETTHUMB = 0x00BE + EM_LINELENGTH = 0x00C1 + EM_REPLACESEL = 0x00C2 + EM_GETLINE = 0x00C4 + EM_LIMITTEXT = 0x00C5 + EM_CANUNDO = 0x00C6 + EM_UNDO = 0x00C7 + EM_FMTLINES = 0x00C8 + EM_LINEFROMCHAR = 0x00C9 + EM_SETTABSTOPS = 0x00CB + EM_SETPASSWORDCHAR = 0x00CC + EM_EMPTYUNDOBUFFER = 0x00CD + EM_GETFIRSTVISIBLELINE = 0x00CE + EM_SETREADONLY = 0x00CF + EM_SETWORDBREAKPROC = 0x00D0 + EM_GETWORDBREAKPROC = 0x00D1 + EM_GETPASSWORDCHAR = 0x00D2 + EM_SETMARGINS = 0x00D3 + EM_GETMARGINS = 0x00D4 + EM_SETLIMITTEXT = EM_LIMITTEXT + EM_GETLIMITTEXT = 0x00D5 + EM_POSFROMCHAR = 0x00D6 + EM_CHARFROMPOS = 0x00D7 + EM_SETIMESTATUS = 0x00D8 + EM_GETIMESTATUS = 0x00D9 + EM_SETCUEBANNER = 0x1501 + EM_GETCUEBANNER = 0x1502 +) + +const ( + CCM_FIRST = 0x2000 + CCM_LAST = CCM_FIRST + 0x200 + CCM_SETBKCOLOR = 8193 + CCM_SETCOLORSCHEME = 8194 + CCM_GETCOLORSCHEME = 8195 + CCM_GETDROPTARGET = 8196 + CCM_SETUNICODEFORMAT = 8197 + CCM_GETUNICODEFORMAT = 8198 + CCM_SETVERSION = 0x2007 + CCM_GETVERSION = 0x2008 + CCM_SETNOTIFYWINDOW = 0x2009 + CCM_SETWINDOWTHEME = 0x200b + CCM_DPISCALE = 0x200c +) + +// Common controls styles +const ( + CCS_TOP = 1 + CCS_NOMOVEY = 2 + CCS_BOTTOM = 3 + CCS_NORESIZE = 4 + CCS_NOPARENTALIGN = 8 + CCS_ADJUSTABLE = 32 + CCS_NODIVIDER = 64 + CCS_VERT = 128 + CCS_LEFT = 129 + CCS_NOMOVEX = 130 + CCS_RIGHT = 131 +) + +// ProgressBar messages +const ( + PROGRESS_CLASS = "msctls_progress32" + PBM_SETPOS = WM_USER + 2 + PBM_DELTAPOS = WM_USER + 3 + PBM_SETSTEP = WM_USER + 4 + PBM_STEPIT = WM_USER + 5 + PBM_SETRANGE32 = 1030 + PBM_GETRANGE = 1031 + PBM_GETPOS = 1032 + PBM_SETBARCOLOR = 1033 + PBM_SETBKCOLOR = CCM_SETBKCOLOR + PBS_SMOOTH = 1 + PBS_VERTICAL = 4 +) + +// Trackbar messages and constants +const ( + TBS_AUTOTICKS = 1 + TBS_VERT = 2 + TBS_HORZ = 0 + TBS_TOP = 4 + TBS_BOTTOM = 0 + TBS_LEFT = 4 + TBS_RIGHT = 0 + TBS_BOTH = 8 + TBS_NOTICKS = 16 + TBS_ENABLESELRANGE = 32 + TBS_FIXEDLENGTH = 64 + TBS_NOTHUMB = 128 + TBS_TOOLTIPS = 0x0100 +) + +const ( + TBM_GETPOS = (WM_USER) + TBM_GETRANGEMIN = (WM_USER + 1) + TBM_GETRANGEMAX = (WM_USER + 2) + TBM_GETTIC = (WM_USER + 3) + TBM_SETTIC = (WM_USER + 4) + TBM_SETPOS = (WM_USER + 5) + TBM_SETRANGE = (WM_USER + 6) + TBM_SETRANGEMIN = (WM_USER + 7) + TBM_SETRANGEMAX = (WM_USER + 8) + TBM_CLEARTICS = (WM_USER + 9) + TBM_SETSEL = (WM_USER + 10) + TBM_SETSELSTART = (WM_USER + 11) + TBM_SETSELEND = (WM_USER + 12) + TBM_GETPTICS = (WM_USER + 14) + TBM_GETTICPOS = (WM_USER + 15) + TBM_GETNUMTICS = (WM_USER + 16) + TBM_GETSELSTART = (WM_USER + 17) + TBM_GETSELEND = (WM_USER + 18) + TBM_CLEARSEL = (WM_USER + 19) + TBM_SETTICFREQ = (WM_USER + 20) + TBM_SETPAGESIZE = (WM_USER + 21) + TBM_GETPAGESIZE = (WM_USER + 22) + TBM_SETLINESIZE = (WM_USER + 23) + TBM_GETLINESIZE = (WM_USER + 24) + TBM_GETTHUMBRECT = (WM_USER + 25) + TBM_GETCHANNELRECT = (WM_USER + 26) + TBM_SETTHUMBLENGTH = (WM_USER + 27) + TBM_GETTHUMBLENGTH = (WM_USER + 28) + TBM_SETTOOLTIPS = (WM_USER + 29) + TBM_GETTOOLTIPS = (WM_USER + 30) + TBM_SETTIPSIDE = (WM_USER + 31) + TBM_SETBUDDY = (WM_USER + 32) + TBM_GETBUDDY = (WM_USER + 33) +) + +const ( + TB_LINEUP = 0 + TB_LINEDOWN = 1 + TB_PAGEUP = 2 + TB_PAGEDOWN = 3 + TB_THUMBPOSITION = 4 + TB_THUMBTRACK = 5 + TB_TOP = 6 + TB_BOTTOM = 7 + TB_ENDTRACK = 8 +) + +// GetOpenFileName and GetSaveFileName extended flags +const ( + OFN_EX_NOPLACESBAR = 0x00000001 +) + +// GetOpenFileName and GetSaveFileName flags +const ( + OFN_ALLOWMULTISELECT = 0x00000200 + OFN_CREATEPROMPT = 0x00002000 + OFN_DONTADDTORECENT = 0x02000000 + OFN_ENABLEHOOK = 0x00000020 + OFN_ENABLEINCLUDENOTIFY = 0x00400000 + OFN_ENABLESIZING = 0x00800000 + OFN_ENABLETEMPLATE = 0x00000040 + OFN_ENABLETEMPLATEHANDLE = 0x00000080 + OFN_EXPLORER = 0x00080000 + OFN_EXTENSIONDIFFERENT = 0x00000400 + OFN_FILEMUSTEXIST = 0x00001000 + OFN_FORCESHOWHIDDEN = 0x10000000 + OFN_HIDEREADONLY = 0x00000004 + OFN_LONGNAMES = 0x00200000 + OFN_NOCHANGEDIR = 0x00000008 + OFN_NODEREFERENCELINKS = 0x00100000 + OFN_NOLONGNAMES = 0x00040000 + OFN_NONETWORKBUTTON = 0x00020000 + OFN_NOREADONLYRETURN = 0x00008000 + OFN_NOTESTFILECREATE = 0x00010000 + OFN_NOVALIDATE = 0x00000100 + OFN_OVERWRITEPROMPT = 0x00000002 + OFN_PATHMUSTEXIST = 0x00000800 + OFN_READONLY = 0x00000001 + OFN_SHAREAWARE = 0x00004000 + OFN_SHOWHELP = 0x00000010 +) + +// SHBrowseForFolder flags +const ( + BIF_RETURNONLYFSDIRS = 0x00000001 + BIF_DONTGOBELOWDOMAIN = 0x00000002 + BIF_STATUSTEXT = 0x00000004 + BIF_RETURNFSANCESTORS = 0x00000008 + BIF_EDITBOX = 0x00000010 + BIF_VALIDATE = 0x00000020 + BIF_NEWDIALOGSTYLE = 0x00000040 + BIF_BROWSEINCLUDEURLS = 0x00000080 + BIF_USENEWUI = BIF_EDITBOX | BIF_NEWDIALOGSTYLE + BIF_UAHINT = 0x00000100 + BIF_NONEWFOLDERBUTTON = 0x00000200 + BIF_NOTRANSLATETARGETS = 0x00000400 + BIF_BROWSEFORCOMPUTER = 0x00001000 + BIF_BROWSEFORPRINTER = 0x00002000 + BIF_BROWSEINCLUDEFILES = 0x00004000 + BIF_SHAREABLE = 0x00008000 + BIF_BROWSEFILEJUNCTIONS = 0x00010000 +) + +// MessageBox flags +const ( + MB_OK = 0x00000000 + MB_OKCANCEL = 0x00000001 + MB_ABORTRETRYIGNORE = 0x00000002 + MB_YESNOCANCEL = 0x00000003 + MB_YESNO = 0x00000004 + MB_RETRYCANCEL = 0x00000005 + MB_CANCELTRYCONTINUE = 0x00000006 + MB_ICONHAND = 0x00000010 + MB_ICONQUESTION = 0x00000020 + MB_ICONEXCLAMATION = 0x00000030 + MB_ICONASTERISK = 0x00000040 + MB_USERICON = 0x00000080 + MB_ICONWARNING = MB_ICONEXCLAMATION + MB_ICONERROR = MB_ICONHAND + MB_ICONINFORMATION = MB_ICONASTERISK + MB_ICONSTOP = MB_ICONHAND + MB_DEFBUTTON1 = 0x00000000 + MB_DEFBUTTON2 = 0x00000100 + MB_DEFBUTTON3 = 0x00000200 + MB_DEFBUTTON4 = 0x00000300 +) + +// COM +const ( + E_INVALIDARG = 0x80070057 + E_OUTOFMEMORY = 0x8007000E + E_UNEXPECTED = 0x8000FFFF +) + +const ( + S_OK = 0 + S_FALSE = 0x0001 + RPC_E_CHANGED_MODE = 0x80010106 +) + +// GetSystemMetrics constants +const ( + SM_CXSCREEN = 0 + SM_CYSCREEN = 1 + SM_CXVSCROLL = 2 + SM_CYHSCROLL = 3 + SM_CYCAPTION = 4 + SM_CXBORDER = 5 + SM_CYBORDER = 6 + SM_CXDLGFRAME = 7 + SM_CYDLGFRAME = 8 + SM_CYVTHUMB = 9 + SM_CXHTHUMB = 10 + SM_CXICON = 11 + SM_CYICON = 12 + SM_CXCURSOR = 13 + SM_CYCURSOR = 14 + SM_CYMENU = 15 + SM_CXFULLSCREEN = 16 + SM_CYFULLSCREEN = 17 + SM_CYKANJIWINDOW = 18 + SM_MOUSEPRESENT = 19 + SM_CYVSCROLL = 20 + SM_CXHSCROLL = 21 + SM_DEBUG = 22 + SM_SWAPBUTTON = 23 + SM_RESERVED1 = 24 + SM_RESERVED2 = 25 + SM_RESERVED3 = 26 + SM_RESERVED4 = 27 + SM_CXMIN = 28 + SM_CYMIN = 29 + SM_CXSIZE = 30 + SM_CYSIZE = 31 + SM_CXFRAME = 32 + SM_CYFRAME = 33 + SM_CXMINTRACK = 34 + SM_CYMINTRACK = 35 + SM_CXDOUBLECLK = 36 + SM_CYDOUBLECLK = 37 + SM_CXICONSPACING = 38 + SM_CYICONSPACING = 39 + SM_MENUDROPALIGNMENT = 40 + SM_PENWINDOWS = 41 + SM_DBCSENABLED = 42 + SM_CMOUSEBUTTONS = 43 + SM_CXFIXEDFRAME = SM_CXDLGFRAME + SM_CYFIXEDFRAME = SM_CYDLGFRAME + SM_CXSIZEFRAME = SM_CXFRAME + SM_CYSIZEFRAME = SM_CYFRAME + SM_SECURE = 44 + SM_CXEDGE = 45 + SM_CYEDGE = 46 + SM_CXMINSPACING = 47 + SM_CYMINSPACING = 48 + SM_CXSMICON = 49 + SM_CYSMICON = 50 + SM_CYSMCAPTION = 51 + SM_CXSMSIZE = 52 + SM_CYSMSIZE = 53 + SM_CXMENUSIZE = 54 + SM_CYMENUSIZE = 55 + SM_ARRANGE = 56 + SM_CXMINIMIZED = 57 + SM_CYMINIMIZED = 58 + SM_CXMAXTRACK = 59 + SM_CYMAXTRACK = 60 + SM_CXMAXIMIZED = 61 + SM_CYMAXIMIZED = 62 + SM_NETWORK = 63 + SM_CLEANBOOT = 67 + SM_CXDRAG = 68 + SM_CYDRAG = 69 + SM_SHOWSOUNDS = 70 + SM_CXMENUCHECK = 71 + SM_CYMENUCHECK = 72 + SM_SLOWMACHINE = 73 + SM_MIDEASTENABLED = 74 + SM_MOUSEWHEELPRESENT = 75 + SM_XVIRTUALSCREEN = 76 + SM_YVIRTUALSCREEN = 77 + SM_CXVIRTUALSCREEN = 78 + SM_CYVIRTUALSCREEN = 79 + SM_CMONITORS = 80 + SM_SAMEDISPLAYFORMAT = 81 + SM_IMMENABLED = 82 + SM_CXFOCUSBORDER = 83 + SM_CYFOCUSBORDER = 84 + SM_TABLETPC = 86 + SM_MEDIACENTER = 87 + SM_STARTER = 88 + SM_SERVERR2 = 89 + SM_CMETRICS = 91 + SM_REMOTESESSION = 0x1000 + SM_SHUTTINGDOWN = 0x2000 + SM_REMOTECONTROL = 0x2001 + SM_CARETBLINKINGENABLED = 0x2002 +) + +const ( + CLSCTX_INPROC_SERVER = 1 + CLSCTX_INPROC_HANDLER = 2 + CLSCTX_LOCAL_SERVER = 4 + CLSCTX_INPROC_SERVER16 = 8 + CLSCTX_REMOTE_SERVER = 16 + CLSCTX_ALL = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER + CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER + CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER +) + +const ( + COINIT_APARTMENTTHREADED = 0x2 + COINIT_MULTITHREADED = 0x0 + COINIT_DISABLE_OLE1DDE = 0x4 + COINIT_SPEED_OVER_MEMORY = 0x8 +) + +const ( + DISPATCH_METHOD = 1 + DISPATCH_PROPERTYGET = 2 + DISPATCH_PROPERTYPUT = 4 + DISPATCH_PROPERTYPUTREF = 8 +) + +const ( + CC_FASTCALL = iota + CC_CDECL + CC_MSCPASCAL + CC_PASCAL = CC_MSCPASCAL + CC_MACPASCAL + CC_STDCALL + CC_FPFASTCALL + CC_SYSCALL + CC_MPWCDECL + CC_MPWPASCAL + CC_MAX = CC_MPWPASCAL +) + +const ( + VT_EMPTY = 0x0 + VT_NULL = 0x1 + VT_I2 = 0x2 + VT_I4 = 0x3 + VT_R4 = 0x4 + VT_R8 = 0x5 + VT_CY = 0x6 + VT_DATE = 0x7 + VT_BSTR = 0x8 + VT_DISPATCH = 0x9 + VT_ERROR = 0xa + VT_BOOL = 0xb + VT_VARIANT = 0xc + VT_UNKNOWN = 0xd + VT_DECIMAL = 0xe + VT_I1 = 0x10 + VT_UI1 = 0x11 + VT_UI2 = 0x12 + VT_UI4 = 0x13 + VT_I8 = 0x14 + VT_UI8 = 0x15 + VT_INT = 0x16 + VT_UINT = 0x17 + VT_VOID = 0x18 + VT_HRESULT = 0x19 + VT_PTR = 0x1a + VT_SAFEARRAY = 0x1b + VT_CARRAY = 0x1c + VT_USERDEFINED = 0x1d + VT_LPSTR = 0x1e + VT_LPWSTR = 0x1f + VT_RECORD = 0x24 + VT_INT_PTR = 0x25 + VT_UINT_PTR = 0x26 + VT_FILETIME = 0x40 + VT_BLOB = 0x41 + VT_STREAM = 0x42 + VT_STORAGE = 0x43 + VT_STREAMED_OBJECT = 0x44 + VT_STORED_OBJECT = 0x45 + VT_BLOB_OBJECT = 0x46 + VT_CF = 0x47 + VT_CLSID = 0x48 + VT_BSTR_BLOB = 0xfff + VT_VECTOR = 0x1000 + VT_ARRAY = 0x2000 + VT_BYREF = 0x4000 + VT_RESERVED = 0x8000 + VT_ILLEGAL = 0xffff + VT_ILLEGALMASKED = 0xfff + VT_TYPEMASK = 0xfff +) + +const ( + DISPID_UNKNOWN = -1 + DISPID_VALUE = 0 + DISPID_PROPERTYPUT = -3 + DISPID_NEWENUM = -4 + DISPID_EVALUATE = -5 + DISPID_CONSTRUCTOR = -6 + DISPID_DESTRUCTOR = -7 + DISPID_COLLECT = -8 +) + +const ( + MONITOR_DEFAULTTONULL = 0x00000000 + MONITOR_DEFAULTTOPRIMARY = 0x00000001 + MONITOR_DEFAULTTONEAREST = 0x00000002 + + MONITORINFOF_PRIMARY = 0x00000001 +) + +const ( + CCHDEVICENAME = 32 + CCHFORMNAME = 32 +) + +const ( + IDOK = 1 + IDCANCEL = 2 + IDABORT = 3 + IDRETRY = 4 + IDIGNORE = 5 + IDYES = 6 + IDNO = 7 + IDCLOSE = 8 + IDHELP = 9 + IDTRYAGAIN = 10 + IDCONTINUE = 11 + IDTIMEOUT = 32000 +) + +// Generic WM_NOTIFY notification codes +const ( + NM_FIRST = 0 + NM_OUTOFMEMORY = NM_FIRST - 1 + NM_CLICK = NM_FIRST - 2 + NM_DBLCLK = NM_FIRST - 3 + NM_RETURN = NM_FIRST - 4 + NM_RCLICK = NM_FIRST - 5 + NM_RDBLCLK = NM_FIRST - 6 + NM_SETFOCUS = NM_FIRST - 7 + NM_KILLFOCUS = NM_FIRST - 8 + NM_CUSTOMDRAW = NM_FIRST - 12 + NM_HOVER = NM_FIRST - 13 + NM_NCHITTEST = NM_FIRST - 14 + NM_KEYDOWN = NM_FIRST - 15 + NM_RELEASEDCAPTURE = NM_FIRST - 16 + NM_SETCURSOR = NM_FIRST - 17 + NM_CHAR = NM_FIRST - 18 + NM_TOOLTIPSCREATED = NM_FIRST - 19 + NM_LAST = NM_FIRST - 99 +) + +// ListView messages +const ( + LVM_FIRST = 0x1000 + LVM_GETITEMCOUNT = LVM_FIRST + 4 + LVM_SETIMAGELIST = LVM_FIRST + 3 + LVM_GETIMAGELIST = LVM_FIRST + 2 + LVM_GETITEM = LVM_FIRST + 75 + LVM_SETITEM = LVM_FIRST + 76 + LVM_INSERTITEM = LVM_FIRST + 77 + LVM_DELETEITEM = LVM_FIRST + 8 + LVM_DELETEALLITEMS = LVM_FIRST + 9 + LVM_GETCALLBACKMASK = LVM_FIRST + 10 + LVM_SETCALLBACKMASK = LVM_FIRST + 11 + LVM_SETUNICODEFORMAT = CCM_SETUNICODEFORMAT + LVM_GETNEXTITEM = LVM_FIRST + 12 + LVM_FINDITEM = LVM_FIRST + 83 + LVM_GETITEMRECT = LVM_FIRST + 14 + LVM_GETSTRINGWIDTH = LVM_FIRST + 87 + LVM_HITTEST = LVM_FIRST + 18 + LVM_ENSUREVISIBLE = LVM_FIRST + 19 + LVM_SCROLL = LVM_FIRST + 20 + LVM_REDRAWITEMS = LVM_FIRST + 21 + LVM_ARRANGE = LVM_FIRST + 22 + LVM_EDITLABEL = LVM_FIRST + 118 + LVM_GETEDITCONTROL = LVM_FIRST + 24 + LVM_GETCOLUMN = LVM_FIRST + 95 + LVM_SETCOLUMN = LVM_FIRST + 96 + LVM_INSERTCOLUMN = LVM_FIRST + 97 + LVM_DELETECOLUMN = LVM_FIRST + 28 + LVM_GETCOLUMNWIDTH = LVM_FIRST + 29 + LVM_SETCOLUMNWIDTH = LVM_FIRST + 30 + LVM_GETHEADER = LVM_FIRST + 31 + LVM_CREATEDRAGIMAGE = LVM_FIRST + 33 + LVM_GETVIEWRECT = LVM_FIRST + 34 + LVM_GETTEXTCOLOR = LVM_FIRST + 35 + LVM_SETTEXTCOLOR = LVM_FIRST + 36 + LVM_GETTEXTBKCOLOR = LVM_FIRST + 37 + LVM_SETTEXTBKCOLOR = LVM_FIRST + 38 + LVM_GETTOPINDEX = LVM_FIRST + 39 + LVM_GETCOUNTPERPAGE = LVM_FIRST + 40 + LVM_GETORIGIN = LVM_FIRST + 41 + LVM_UPDATE = LVM_FIRST + 42 + LVM_SETITEMSTATE = LVM_FIRST + 43 + LVM_GETITEMSTATE = LVM_FIRST + 44 + LVM_GETITEMTEXT = LVM_FIRST + 115 + LVM_SETITEMTEXT = LVM_FIRST + 116 + LVM_SETITEMCOUNT = LVM_FIRST + 47 + LVM_SORTITEMS = LVM_FIRST + 48 + LVM_SETITEMPOSITION32 = LVM_FIRST + 49 + LVM_GETSELECTEDCOUNT = LVM_FIRST + 50 + LVM_GETITEMSPACING = LVM_FIRST + 51 + LVM_GETISEARCHSTRING = LVM_FIRST + 117 + LVM_SETICONSPACING = LVM_FIRST + 53 + LVM_SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54 + LVM_GETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 55 + LVM_GETSUBITEMRECT = LVM_FIRST + 56 + LVM_SUBITEMHITTEST = LVM_FIRST + 57 + LVM_SETCOLUMNORDERARRAY = LVM_FIRST + 58 + LVM_GETCOLUMNORDERARRAY = LVM_FIRST + 59 + LVM_SETHOTITEM = LVM_FIRST + 60 + LVM_GETHOTITEM = LVM_FIRST + 61 + LVM_SETHOTCURSOR = LVM_FIRST + 62 + LVM_GETHOTCURSOR = LVM_FIRST + 63 + LVM_APPROXIMATEVIEWRECT = LVM_FIRST + 64 + LVM_SETWORKAREAS = LVM_FIRST + 65 + LVM_GETWORKAREAS = LVM_FIRST + 70 + LVM_GETNUMBEROFWORKAREAS = LVM_FIRST + 73 + LVM_GETSELECTIONMARK = LVM_FIRST + 66 + LVM_SETSELECTIONMARK = LVM_FIRST + 67 + LVM_SETHOVERTIME = LVM_FIRST + 71 + LVM_GETHOVERTIME = LVM_FIRST + 72 + LVM_SETTOOLTIPS = LVM_FIRST + 74 + LVM_GETTOOLTIPS = LVM_FIRST + 78 + LVM_SORTITEMSEX = LVM_FIRST + 81 + LVM_SETBKIMAGE = LVM_FIRST + 138 + LVM_GETBKIMAGE = LVM_FIRST + 139 + LVM_SETSELECTEDCOLUMN = LVM_FIRST + 140 + LVM_SETVIEW = LVM_FIRST + 142 + LVM_GETVIEW = LVM_FIRST + 143 + LVM_INSERTGROUP = LVM_FIRST + 145 + LVM_SETGROUPINFO = LVM_FIRST + 147 + LVM_GETGROUPINFO = LVM_FIRST + 149 + LVM_REMOVEGROUP = LVM_FIRST + 150 + LVM_MOVEGROUP = LVM_FIRST + 151 + LVM_GETGROUPCOUNT = LVM_FIRST + 152 + LVM_GETGROUPINFOBYINDEX = LVM_FIRST + 153 + LVM_MOVEITEMTOGROUP = LVM_FIRST + 154 + LVM_GETGROUPRECT = LVM_FIRST + 98 + LVM_SETGROUPMETRICS = LVM_FIRST + 155 + LVM_GETGROUPMETRICS = LVM_FIRST + 156 + LVM_ENABLEGROUPVIEW = LVM_FIRST + 157 + LVM_SORTGROUPS = LVM_FIRST + 158 + LVM_INSERTGROUPSORTED = LVM_FIRST + 159 + LVM_REMOVEALLGROUPS = LVM_FIRST + 160 + LVM_HASGROUP = LVM_FIRST + 161 + LVM_GETGROUPSTATE = LVM_FIRST + 92 + LVM_GETFOCUSEDGROUP = LVM_FIRST + 93 + LVM_SETTILEVIEWINFO = LVM_FIRST + 162 + LVM_GETTILEVIEWINFO = LVM_FIRST + 163 + LVM_SETTILEINFO = LVM_FIRST + 164 + LVM_GETTILEINFO = LVM_FIRST + 165 + LVM_SETINSERTMARK = LVM_FIRST + 166 + LVM_GETINSERTMARK = LVM_FIRST + 167 + LVM_INSERTMARKHITTEST = LVM_FIRST + 168 + LVM_GETINSERTMARKRECT = LVM_FIRST + 169 + LVM_SETINSERTMARKCOLOR = LVM_FIRST + 170 + LVM_GETINSERTMARKCOLOR = LVM_FIRST + 171 + LVM_SETINFOTIP = LVM_FIRST + 173 + LVM_GETSELECTEDCOLUMN = LVM_FIRST + 174 + LVM_ISGROUPVIEWENABLED = LVM_FIRST + 175 + LVM_GETOUTLINECOLOR = LVM_FIRST + 176 + LVM_SETOUTLINECOLOR = LVM_FIRST + 177 + LVM_CANCELEDITLABEL = LVM_FIRST + 179 + LVM_MAPINDEXTOID = LVM_FIRST + 180 + LVM_MAPIDTOINDEX = LVM_FIRST + 181 + LVM_ISITEMVISIBLE = LVM_FIRST + 182 + LVM_GETNEXTITEMINDEX = LVM_FIRST + 211 +) + +// ListView notifications +const ( + LVN_FIRST = -100 + + LVN_ITEMCHANGING = LVN_FIRST - 0 + LVN_ITEMCHANGED = LVN_FIRST - 1 + LVN_INSERTITEM = LVN_FIRST - 2 + LVN_DELETEITEM = LVN_FIRST - 3 + LVN_DELETEALLITEMS = LVN_FIRST - 4 + LVN_BEGINLABELEDITA = LVN_FIRST - 5 + LVN_BEGINLABELEDITW = LVN_FIRST - 75 + LVN_ENDLABELEDITA = LVN_FIRST - 6 + LVN_ENDLABELEDITW = LVN_FIRST - 76 + LVN_COLUMNCLICK = LVN_FIRST - 8 + LVN_BEGINDRAG = LVN_FIRST - 9 + LVN_BEGINRDRAG = LVN_FIRST - 11 + LVN_ODCACHEHINT = LVN_FIRST - 13 + LVN_ODFINDITEMA = LVN_FIRST - 52 + LVN_ODFINDITEMW = LVN_FIRST - 79 + LVN_ITEMACTIVATE = LVN_FIRST - 14 + LVN_ODSTATECHANGED = LVN_FIRST - 15 + LVN_HOTTRACK = LVN_FIRST - 21 + LVN_GETDISPINFO = LVN_FIRST - 77 + LVN_SETDISPINFO = LVN_FIRST - 78 + LVN_KEYDOWN = LVN_FIRST - 55 + LVN_MARQUEEBEGIN = LVN_FIRST - 56 + LVN_GETINFOTIP = LVN_FIRST - 58 + LVN_INCREMENTALSEARCH = LVN_FIRST - 63 + LVN_BEGINSCROLL = LVN_FIRST - 80 + LVN_ENDSCROLL = LVN_FIRST - 81 +) + +const ( + LVSCW_AUTOSIZE = ^uintptr(0) + LVSCW_AUTOSIZE_USEHEADER = ^uintptr(1) +) + +// ListView LVNI constants +const ( + LVNI_ALL = 0 + LVNI_FOCUSED = 1 + LVNI_SELECTED = 2 + LVNI_CUT = 4 + LVNI_DROPHILITED = 8 + LVNI_ABOVE = 256 + LVNI_BELOW = 512 + LVNI_TOLEFT = 1024 + LVNI_TORIGHT = 2048 +) + +// ListView styles +const ( + LVS_ICON = 0x0000 + LVS_REPORT = 0x0001 + LVS_SMALLICON = 0x0002 + LVS_LIST = 0x0003 + LVS_TYPEMASK = 0x0003 + LVS_SINGLESEL = 0x0004 + LVS_SHOWSELALWAYS = 0x0008 + LVS_SORTASCENDING = 0x0010 + LVS_SORTDESCENDING = 0x0020 + LVS_SHAREIMAGELISTS = 0x0040 + LVS_NOLABELWRAP = 0x0080 + LVS_AUTOARRANGE = 0x0100 + LVS_EDITLABELS = 0x0200 + LVS_OWNERDATA = 0x1000 + LVS_NOSCROLL = 0x2000 + LVS_TYPESTYLEMASK = 0xfc00 + LVS_ALIGNTOP = 0x0000 + LVS_ALIGNLEFT = 0x0800 + LVS_ALIGNMASK = 0x0c00 + LVS_OWNERDRAWFIXED = 0x0400 + LVS_NOCOLUMNHEADER = 0x4000 + LVS_NOSORTHEADER = 0x8000 +) + +// ListView extended styles +const ( + LVS_EX_GRIDLINES = 0x00000001 + LVS_EX_SUBITEMIMAGES = 0x00000002 + LVS_EX_CHECKBOXES = 0x00000004 + LVS_EX_TRACKSELECT = 0x00000008 + LVS_EX_HEADERDRAGDROP = 0x00000010 + LVS_EX_FULLROWSELECT = 0x00000020 + LVS_EX_ONECLICKACTIVATE = 0x00000040 + LVS_EX_TWOCLICKACTIVATE = 0x00000080 + LVS_EX_FLATSB = 0x00000100 + LVS_EX_REGIONAL = 0x00000200 + LVS_EX_INFOTIP = 0x00000400 + LVS_EX_UNDERLINEHOT = 0x00000800 + LVS_EX_UNDERLINECOLD = 0x00001000 + LVS_EX_MULTIWORKAREAS = 0x00002000 + LVS_EX_LABELTIP = 0x00004000 + LVS_EX_BORDERSELECT = 0x00008000 + LVS_EX_DOUBLEBUFFER = 0x00010000 + LVS_EX_HIDELABELS = 0x00020000 + LVS_EX_SINGLEROW = 0x00040000 + LVS_EX_SNAPTOGRID = 0x00080000 + LVS_EX_SIMPLESELECT = 0x00100000 +) + +// ListView column flags +const ( + LVCF_FMT = 0x0001 + LVCF_WIDTH = 0x0002 + LVCF_TEXT = 0x0004 + LVCF_SUBITEM = 0x0008 + LVCF_IMAGE = 0x0010 + LVCF_ORDER = 0x0020 +) + +// ListView column format constants +const ( + LVCFMT_LEFT = 0x0000 + LVCFMT_RIGHT = 0x0001 + LVCFMT_CENTER = 0x0002 + LVCFMT_JUSTIFYMASK = 0x0003 + LVCFMT_IMAGE = 0x0800 + LVCFMT_BITMAP_ON_RIGHT = 0x1000 + LVCFMT_COL_HAS_IMAGES = 0x8000 +) + +// ListView item flags +const ( + LVIF_TEXT = 0x00000001 + LVIF_IMAGE = 0x00000002 + LVIF_PARAM = 0x00000004 + LVIF_STATE = 0x00000008 + LVIF_INDENT = 0x00000010 + LVIF_NORECOMPUTE = 0x00000800 + LVIF_GROUPID = 0x00000100 + LVIF_COLUMNS = 0x00000200 +) + +const LVFI_PARAM = 0x0001 + +// ListView item states +const ( + LVIS_FOCUSED = 1 + LVIS_SELECTED = 2 + LVIS_CUT = 4 + LVIS_DROPHILITED = 8 + LVIS_OVERLAYMASK = 0xF00 + LVIS_STATEIMAGEMASK = 0xF000 +) + +// ListView hit test constants +const ( + LVHT_NOWHERE = 0x00000001 + LVHT_ONITEMICON = 0x00000002 + LVHT_ONITEMLABEL = 0x00000004 + LVHT_ONITEMSTATEICON = 0x00000008 + LVHT_ONITEM = LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON + + LVHT_ABOVE = 0x00000008 + LVHT_BELOW = 0x00000010 + LVHT_TORIGHT = 0x00000020 + LVHT_TOLEFT = 0x00000040 +) + +// ListView image list types +const ( + LVSIL_NORMAL = 0 + LVSIL_SMALL = 1 + LVSIL_STATE = 2 + LVSIL_GROUPHEADER = 3 +) + +// InitCommonControlsEx flags +const ( + ICC_LISTVIEW_CLASSES = 1 + ICC_TREEVIEW_CLASSES = 2 + ICC_BAR_CLASSES = 4 + ICC_TAB_CLASSES = 8 + ICC_UPDOWN_CLASS = 16 + ICC_PROGRESS_CLASS = 32 + ICC_HOTKEY_CLASS = 64 + ICC_ANIMATE_CLASS = 128 + ICC_WIN95_CLASSES = 255 + ICC_DATE_CLASSES = 256 + ICC_USEREX_CLASSES = 512 + ICC_COOL_CLASSES = 1024 + ICC_INTERNET_CLASSES = 2048 + ICC_PAGESCROLLER_CLASS = 4096 + ICC_NATIVEFNTCTL_CLASS = 8192 + INFOTIPSIZE = 1024 + ICC_STANDARD_CLASSES = 0x00004000 + ICC_LINK_CLASS = 0x00008000 +) + +// Dialog Codes +const ( + DLGC_WANTARROWS = 0x0001 + DLGC_WANTTAB = 0x0002 + DLGC_WANTALLKEYS = 0x0004 + DLGC_WANTMESSAGE = 0x0004 + DLGC_HASSETSEL = 0x0008 + DLGC_DEFPUSHBUTTON = 0x0010 + DLGC_UNDEFPUSHBUTTON = 0x0020 + DLGC_RADIOBUTTON = 0x0040 + DLGC_WANTCHARS = 0x0080 + DLGC_STATIC = 0x0100 + DLGC_BUTTON = 0x2000 +) + +// Get/SetWindowWord/Long offsets for use with WC_DIALOG windows +const ( + DWL_MSGRESULT = 0 + DWL_DLGPROC = 4 + DWL_USER = 8 +) + +// Registry predefined keys +const ( + HKEY_CLASSES_ROOT HKEY = 0x80000000 + HKEY_CURRENT_USER HKEY = 0x80000001 + HKEY_LOCAL_MACHINE HKEY = 0x80000002 + HKEY_USERS HKEY = 0x80000003 + HKEY_PERFORMANCE_DATA HKEY = 0x80000004 + HKEY_CURRENT_CONFIG HKEY = 0x80000005 + HKEY_DYN_DATA HKEY = 0x80000006 +) + +// Registry Key Security and Access Rights +const ( + KEY_ALL_ACCESS = 0xF003F + KEY_CREATE_SUB_KEY = 0x0004 + KEY_ENUMERATE_SUB_KEYS = 0x0008 + KEY_NOTIFY = 0x0010 + KEY_QUERY_VALUE = 0x0001 + KEY_SET_VALUE = 0x0002 + KEY_READ = 0x20019 + KEY_WRITE = 0x20006 +) + +const ( + NFR_ANSI = 1 + NFR_UNICODE = 2 + NF_QUERY = 3 + NF_REQUERY = 4 +) + +// Registry value types +const ( + RRF_RT_REG_NONE = 0x00000001 + RRF_RT_REG_SZ = 0x00000002 + RRF_RT_REG_EXPAND_SZ = 0x00000004 + RRF_RT_REG_BINARY = 0x00000008 + RRF_RT_REG_DWORD = 0x00000010 + RRF_RT_REG_MULTI_SZ = 0x00000020 + RRF_RT_REG_QWORD = 0x00000040 + RRF_RT_DWORD = (RRF_RT_REG_BINARY | RRF_RT_REG_DWORD) + RRF_RT_QWORD = (RRF_RT_REG_BINARY | RRF_RT_REG_QWORD) + RRF_RT_ANY = 0x0000ffff + RRF_NOEXPAND = 0x10000000 + RRF_ZEROONFAILURE = 0x20000000 + REG_PROCESS_APPKEY = 0x00000001 + REG_MUI_STRING_TRUNCATE = 0x00000001 +) + +// PeekMessage wRemoveMsg value +const ( + PM_NOREMOVE = 0x000 + PM_REMOVE = 0x001 + PM_NOYIELD = 0x002 +) + +// ImageList flags +const ( + ILC_MASK = 0x00000001 + ILC_COLOR = 0x00000000 + ILC_COLORDDB = 0x000000FE + ILC_COLOR4 = 0x00000004 + ILC_COLOR8 = 0x00000008 + ILC_COLOR16 = 0x00000010 + ILC_COLOR24 = 0x00000018 + ILC_COLOR32 = 0x00000020 + ILC_PALETTE = 0x00000800 + ILC_MIRROR = 0x00002000 + ILC_PERITEMMIRROR = 0x00008000 + ILC_ORIGINALSIZE = 0x00010000 + ILC_HIGHQUALITYSCALE = 0x00020000 +) + +// Keystroke Message Flags +const ( + KF_EXTENDED = 0x0100 + KF_DLGMODE = 0x0800 + KF_MENUMODE = 0x1000 + KF_ALTDOWN = 0x2000 + KF_REPEAT = 0x4000 + KF_UP = 0x8000 +) + +// Virtual-Key Codes +/* +const ( + VK_LBUTTON = 0x01 + VK_RBUTTON = 0x02 + VK_CANCEL = 0x03 + VK_MBUTTON = 0x04 + VK_XBUTTON1 = 0x05 + VK_XBUTTON2 = 0x06 + VK_BACK = 0x08 + VK_TAB = 0x09 + VK_CLEAR = 0x0C + VK_RETURN = 0x0D + VK_SHIFT = 0x10 + VK_CONTROL = 0x11 + VK_MENU = 0x12 + VK_PAUSE = 0x13 + VK_CAPITAL = 0x14 + VK_KANA = 0x15 + VK_HANGEUL = 0x15 + VK_HANGUL = 0x15 + VK_JUNJA = 0x17 + VK_FINAL = 0x18 + VK_HANJA = 0x19 + VK_KANJI = 0x19 + VK_ESCAPE = 0x1B + VK_CONVERT = 0x1C + VK_NONCONVERT = 0x1D + VK_ACCEPT = 0x1E + VK_MODECHANGE = 0x1F + VK_SPACE = 0x20 + VK_PRIOR = 0x21 + VK_NEXT = 0x22 + VK_END = 0x23 + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_UP = 0x26 + VK_RIGHT = 0x27 + VK_DOWN = 0x28 + VK_SELECT = 0x29 + VK_PRINT = 0x2A + VK_EXECUTE = 0x2B + VK_SNAPSHOT = 0x2C + VK_INSERT = 0x2D + VK_DELETE = 0x2E + VK_HELP = 0x2F + VK_LWIN = 0x5B + VK_RWIN = 0x5C + VK_APPS = 0x5D + VK_SLEEP = 0x5F + VK_NUMPAD0 = 0x60 + VK_NUMPAD1 = 0x61 + VK_NUMPAD2 = 0x62 + VK_NUMPAD3 = 0x63 + VK_NUMPAD4 = 0x64 + VK_NUMPAD5 = 0x65 + VK_NUMPAD6 = 0x66 + VK_NUMPAD7 = 0x67 + VK_NUMPAD8 = 0x68 + VK_NUMPAD9 = 0x69 + VK_MULTIPLY = 0x6A + VK_ADD = 0x6B + VK_SEPARATOR = 0x6C + VK_SUBTRACT = 0x6D + VK_DECIMAL = 0x6E + VK_DIVIDE = 0x6F + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + VK_F13 = 0x7C + VK_F14 = 0x7D + VK_F15 = 0x7E + VK_F16 = 0x7F + VK_F17 = 0x80 + VK_F18 = 0x81 + VK_F19 = 0x82 + VK_F20 = 0x83 + VK_F21 = 0x84 + VK_F22 = 0x85 + VK_F23 = 0x86 + VK_F24 = 0x87 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_OEM_NEC_EQUAL = 0x92 + VK_OEM_FJ_JISHO = 0x92 + VK_OEM_FJ_MASSHOU = 0x93 + VK_OEM_FJ_TOUROKU = 0x94 + VK_OEM_FJ_LOYA = 0x95 + VK_OEM_FJ_ROYA = 0x96 + VK_LSHIFT = 0xA0 + VK_RSHIFT = 0xA1 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + VK_LMENU = 0xA4 + VK_RMENU = 0xA5 + VK_BROWSER_BACK = 0xA6 + VK_BROWSER_FORWARD = 0xA7 + VK_BROWSER_REFRESH = 0xA8 + VK_BROWSER_STOP = 0xA9 + VK_BROWSER_SEARCH = 0xAA + VK_BROWSER_FAVORITES = 0xAB + VK_BROWSER_HOME = 0xAC + VK_VOLUME_MUTE = 0xAD + VK_VOLUME_DOWN = 0xAE + VK_VOLUME_UP = 0xAF + VK_MEDIA_NEXT_TRACK = 0xB0 + VK_MEDIA_PREV_TRACK = 0xB1 + VK_MEDIA_STOP = 0xB2 + VK_MEDIA_PLAY_PAUSE = 0xB3 + VK_LAUNCH_MAIL = 0xB4 + VK_LAUNCH_MEDIA_SELECT = 0xB5 + VK_LAUNCH_APP1 = 0xB6 + VK_LAUNCH_APP2 = 0xB7 + VK_OEM_1 = 0xBA + VK_OEM_PLUS = 0xBB + VK_OEM_COMMA = 0xBC + VK_OEM_MINUS = 0xBD + VK_OEM_PERIOD = 0xBE + VK_OEM_2 = 0xBF + VK_OEM_3 = 0xC0 + VK_OEM_4 = 0xDB + VK_OEM_5 = 0xDC + VK_OEM_6 = 0xDD + VK_OEM_7 = 0xDE + VK_OEM_8 = 0xDF + VK_OEM_AX = 0xE1 + VK_OEM_102 = 0xE2 + VK_ICO_HELP = 0xE3 + VK_ICO_00 = 0xE4 + VK_PROCESSKEY = 0xE5 + VK_ICO_CLEAR = 0xE6 + VK_OEM_RESET = 0xE9 + VK_OEM_JUMP = 0xEA + VK_OEM_PA1 = 0xEB + VK_OEM_PA2 = 0xEC + VK_OEM_PA3 = 0xED + VK_OEM_WSCTRL = 0xEE + VK_OEM_CUSEL = 0xEF + VK_OEM_ATTN = 0xF0 + VK_OEM_FINISH = 0xF1 + VK_OEM_COPY = 0xF2 + VK_OEM_AUTO = 0xF3 + VK_OEM_ENLW = 0xF4 + VK_OEM_BACKTAB = 0xF5 + VK_ATTN = 0xF6 + VK_CRSEL = 0xF7 + VK_EXSEL = 0xF8 + VK_EREOF = 0xF9 + VK_PLAY = 0xFA + VK_ZOOM = 0xFB + VK_NONAME = 0xFC + VK_PA1 = 0xFD + VK_OEM_CLEAR = 0xFE +)*/ + +// Registry Value Types +const ( + REG_NONE = 0 + REG_SZ = 1 + REG_EXPAND_SZ = 2 + REG_BINARY = 3 + REG_DWORD = 4 + REG_DWORD_LITTLE_ENDIAN = 4 + REG_DWORD_BIG_ENDIAN = 5 + REG_LINK = 6 + REG_MULTI_SZ = 7 + REG_RESOURCE_LIST = 8 + REG_FULL_RESOURCE_DESCRIPTOR = 9 + REG_RESOURCE_REQUIREMENTS_LIST = 10 + REG_QWORD = 11 + REG_QWORD_LITTLE_ENDIAN = 11 +) + +// Tooltip styles +const ( + TTS_ALWAYSTIP = 0x01 + TTS_NOPREFIX = 0x02 + TTS_NOANIMATE = 0x10 + TTS_NOFADE = 0x20 + TTS_BALLOON = 0x40 + TTS_CLOSE = 0x80 + TTS_USEVISUALSTYLE = 0x100 +) + +// Tooltip messages +const ( + TTM_ACTIVATE = (WM_USER + 1) + TTM_SETDELAYTIME = (WM_USER + 3) + TTM_ADDTOOL = (WM_USER + 50) + TTM_DELTOOL = (WM_USER + 51) + TTM_NEWTOOLRECT = (WM_USER + 52) + TTM_RELAYEVENT = (WM_USER + 7) + TTM_GETTOOLINFO = (WM_USER + 53) + TTM_SETTOOLINFO = (WM_USER + 54) + TTM_HITTEST = (WM_USER + 55) + TTM_GETTEXT = (WM_USER + 56) + TTM_UPDATETIPTEXT = (WM_USER + 57) + TTM_GETTOOLCOUNT = (WM_USER + 13) + TTM_ENUMTOOLS = (WM_USER + 58) + TTM_GETCURRENTTOOL = (WM_USER + 59) + TTM_WINDOWFROMPOINT = (WM_USER + 16) + TTM_TRACKACTIVATE = (WM_USER + 17) + TTM_TRACKPOSITION = (WM_USER + 18) + TTM_SETTIPBKCOLOR = (WM_USER + 19) + TTM_SETTIPTEXTCOLOR = (WM_USER + 20) + TTM_GETDELAYTIME = (WM_USER + 21) + TTM_GETTIPBKCOLOR = (WM_USER + 22) + TTM_GETTIPTEXTCOLOR = (WM_USER + 23) + TTM_SETMAXTIPWIDTH = (WM_USER + 24) + TTM_GETMAXTIPWIDTH = (WM_USER + 25) + TTM_SETMARGIN = (WM_USER + 26) + TTM_GETMARGIN = (WM_USER + 27) + TTM_POP = (WM_USER + 28) + TTM_UPDATE = (WM_USER + 29) + TTM_GETBUBBLESIZE = (WM_USER + 30) + TTM_ADJUSTRECT = (WM_USER + 31) + TTM_SETTITLE = (WM_USER + 33) + TTM_POPUP = (WM_USER + 34) + TTM_GETTITLE = (WM_USER + 35) +) + +// Tooltip icons +const ( + TTI_NONE = 0 + TTI_INFO = 1 + TTI_WARNING = 2 + TTI_ERROR = 3 + TTI_INFO_LARGE = 4 + TTI_WARNING_LARGE = 5 + TTI_ERROR_LARGE = 6 +) + +// Tooltip notifications +const ( + TTN_FIRST = -520 + TTN_LAST = -549 + TTN_GETDISPINFO = (TTN_FIRST - 10) + TTN_SHOW = (TTN_FIRST - 1) + TTN_POP = (TTN_FIRST - 2) + TTN_LINKCLICK = (TTN_FIRST - 3) + TTN_NEEDTEXT = TTN_GETDISPINFO +) + +const ( + TTF_IDISHWND = 0x0001 + TTF_CENTERTIP = 0x0002 + TTF_RTLREADING = 0x0004 + TTF_SUBCLASS = 0x0010 + TTF_TRACK = 0x0020 + TTF_ABSOLUTE = 0x0080 + TTF_TRANSPARENT = 0x0100 + TTF_PARSELINKS = 0x1000 + TTF_DI_SETITEM = 0x8000 +) + +const ( + SWP_NOSIZE = 0x0001 + SWP_NOMOVE = 0x0002 + SWP_NOZORDER = 0x0004 + SWP_NOREDRAW = 0x0008 + SWP_NOACTIVATE = 0x0010 + SWP_FRAMECHANGED = 0x0020 + SWP_SHOWWINDOW = 0x0040 + SWP_HIDEWINDOW = 0x0080 + SWP_NOCOPYBITS = 0x0100 + SWP_NOOWNERZORDER = 0x0200 + SWP_NOSENDCHANGING = 0x0400 + SWP_DRAWFRAME = SWP_FRAMECHANGED + SWP_NOREPOSITION = SWP_NOOWNERZORDER + SWP_DEFERERASE = 0x2000 + SWP_ASYNCWINDOWPOS = 0x4000 +) + +// Predefined window handles +const ( + HWND_BROADCAST = HWND(0xFFFF) + HWND_BOTTOM = HWND(1) + HWND_NOTOPMOST = ^HWND(1) // -2 + HWND_TOP = HWND(0) + HWND_TOPMOST = ^HWND(0) // -1 + HWND_DESKTOP = HWND(0) + HWND_MESSAGE = ^HWND(2) // -3 +) + +// Pen types +const ( + PS_COSMETIC = 0x00000000 + PS_GEOMETRIC = 0x00010000 + PS_TYPE_MASK = 0x000F0000 +) + +// Pen styles +const ( + PS_SOLID = 0 + PS_DASH = 1 + PS_DOT = 2 + PS_DASHDOT = 3 + PS_DASHDOTDOT = 4 + PS_NULL = 5 + PS_INSIDEFRAME = 6 + PS_USERSTYLE = 7 + PS_ALTERNATE = 8 + PS_STYLE_MASK = 0x0000000F +) + +// Pen cap types +const ( + PS_ENDCAP_ROUND = 0x00000000 + PS_ENDCAP_SQUARE = 0x00000100 + PS_ENDCAP_FLAT = 0x00000200 + PS_ENDCAP_MASK = 0x00000F00 +) + +// Pen join types +const ( + PS_JOIN_ROUND = 0x00000000 + PS_JOIN_BEVEL = 0x00001000 + PS_JOIN_MITER = 0x00002000 + PS_JOIN_MASK = 0x0000F000 +) + +// Hatch styles +const ( + HS_HORIZONTAL = 0 + HS_VERTICAL = 1 + HS_FDIAGONAL = 2 + HS_BDIAGONAL = 3 + HS_CROSS = 4 + HS_DIAGCROSS = 5 +) + +// Stock Logical Objects +const ( + WHITE_BRUSH = 0 + LTGRAY_BRUSH = 1 + GRAY_BRUSH = 2 + DKGRAY_BRUSH = 3 + BLACK_BRUSH = 4 + NULL_BRUSH = 5 + HOLLOW_BRUSH = NULL_BRUSH + WHITE_PEN = 6 + BLACK_PEN = 7 + NULL_PEN = 8 + OEM_FIXED_FONT = 10 + ANSI_FIXED_FONT = 11 + ANSI_VAR_FONT = 12 + SYSTEM_FONT = 13 + DEVICE_DEFAULT_FONT = 14 + DEFAULT_PALETTE = 15 + SYSTEM_FIXED_FONT = 16 + DEFAULT_GUI_FONT = 17 + DC_BRUSH = 18 + DC_PEN = 19 +) + +// Brush styles +const ( + BS_SOLID = 0 + BS_NULL = 1 + BS_HOLLOW = BS_NULL + BS_HATCHED = 2 + BS_PATTERN = 3 + BS_INDEXED = 4 + BS_DIBPATTERN = 5 + BS_DIBPATTERNPT = 6 + BS_PATTERN8X8 = 7 + BS_DIBPATTERN8X8 = 8 + BS_MONOPATTERN = 9 +) + +// TRACKMOUSEEVENT flags +const ( + TME_HOVER = 0x00000001 + TME_LEAVE = 0x00000002 + TME_NONCLIENT = 0x00000010 + TME_QUERY = 0x40000000 + TME_CANCEL = 0x80000000 + + HOVER_DEFAULT = 0xFFFFFFFF +) + +// WM_NCHITTEST and MOUSEHOOKSTRUCT Mouse Position Codes +const ( + HTERROR = (-2) + HTTRANSPARENT = (-1) + HTNOWHERE = 0 + HTCLIENT = 1 + HTCAPTION = 2 + HTSYSMENU = 3 + HTGROWBOX = 4 + HTSIZE = HTGROWBOX + HTMENU = 5 + HTHSCROLL = 6 + HTVSCROLL = 7 + HTMINBUTTON = 8 + HTMAXBUTTON = 9 + HTLEFT = 10 + HTRIGHT = 11 + HTTOP = 12 + HTTOPLEFT = 13 + HTTOPRIGHT = 14 + HTBOTTOM = 15 + HTBOTTOMLEFT = 16 + HTBOTTOMRIGHT = 17 + HTBORDER = 18 + HTREDUCE = HTMINBUTTON + HTZOOM = HTMAXBUTTON + HTSIZEFIRST = HTLEFT + HTSIZELAST = HTBOTTOMRIGHT + HTOBJECT = 19 + HTCLOSE = 20 + HTHELP = 21 +) + +// DrawText[Ex] format flags +const ( + DT_TOP = 0x00000000 + DT_LEFT = 0x00000000 + DT_CENTER = 0x00000001 + DT_RIGHT = 0x00000002 + DT_VCENTER = 0x00000004 + DT_BOTTOM = 0x00000008 + DT_WORDBREAK = 0x00000010 + DT_SINGLELINE = 0x00000020 + DT_EXPANDTABS = 0x00000040 + DT_TABSTOP = 0x00000080 + DT_NOCLIP = 0x00000100 + DT_EXTERNALLEADING = 0x00000200 + DT_CALCRECT = 0x00000400 + DT_NOPREFIX = 0x00000800 + DT_INTERNAL = 0x00001000 + DT_EDITCONTROL = 0x00002000 + DT_PATH_ELLIPSIS = 0x00004000 + DT_END_ELLIPSIS = 0x00008000 + DT_MODIFYSTRING = 0x00010000 + DT_RTLREADING = 0x00020000 + DT_WORD_ELLIPSIS = 0x00040000 + DT_NOFULLWIDTHCHARBREAK = 0x00080000 + DT_HIDEPREFIX = 0x00100000 + DT_PREFIXONLY = 0x00200000 +) + +const CLR_INVALID = 0xFFFFFFFF + +// Background Modes +const ( + TRANSPARENT = 1 + OPAQUE = 2 + BKMODE_LAST = 2 +) + +// Global Memory Flags +const ( + GMEM_FIXED = 0x0000 + GMEM_MOVEABLE = 0x0002 + GMEM_NOCOMPACT = 0x0010 + GMEM_NODISCARD = 0x0020 + GMEM_ZEROINIT = 0x0040 + GMEM_MODIFY = 0x0080 + GMEM_DISCARDABLE = 0x0100 + GMEM_NOT_BANKED = 0x1000 + GMEM_SHARE = 0x2000 + GMEM_DDESHARE = 0x2000 + GMEM_NOTIFY = 0x4000 + GMEM_LOWER = GMEM_NOT_BANKED + GMEM_VALID_FLAGS = 0x7F72 + GMEM_INVALID_HANDLE = 0x8000 + GHND = (GMEM_MOVEABLE | GMEM_ZEROINIT) + GPTR = (GMEM_FIXED | GMEM_ZEROINIT) +) + +// Ternary raster operations +const ( + SRCCOPY = 0x00CC0020 + SRCPAINT = 0x00EE0086 + SRCAND = 0x008800C6 + SRCINVERT = 0x00660046 + SRCERASE = 0x00440328 + NOTSRCCOPY = 0x00330008 + NOTSRCERASE = 0x001100A6 + MERGECOPY = 0x00C000CA + MERGEPAINT = 0x00BB0226 + PATCOPY = 0x00F00021 + PATPAINT = 0x00FB0A09 + PATINVERT = 0x005A0049 + DSTINVERT = 0x00550009 + BLACKNESS = 0x00000042 + WHITENESS = 0x00FF0062 + NOMIRRORBITMAP = 0x80000000 + CAPTUREBLT = 0x40000000 +) + +// Clipboard formats +const ( + CF_TEXT = 1 + CF_BITMAP = 2 + CF_METAFILEPICT = 3 + CF_SYLK = 4 + CF_DIF = 5 + CF_TIFF = 6 + CF_OEMTEXT = 7 + CF_DIB = 8 + CF_PALETTE = 9 + CF_PENDATA = 10 + CF_RIFF = 11 + CF_WAVE = 12 + CF_UNICODETEXT = 13 + CF_ENHMETAFILE = 14 + CF_HDROP = 15 + CF_LOCALE = 16 + CF_DIBV5 = 17 + CF_MAX = 18 + CF_OWNERDISPLAY = 0x0080 + CF_DSPTEXT = 0x0081 + CF_DSPBITMAP = 0x0082 + CF_DSPMETAFILEPICT = 0x0083 + CF_DSPENHMETAFILE = 0x008E + CF_PRIVATEFIRST = 0x0200 + CF_PRIVATELAST = 0x02FF + CF_GDIOBJFIRST = 0x0300 + CF_GDIOBJLAST = 0x03FF +) + +// Bitmap compression formats +const ( + BI_RGB = 0 + BI_RLE8 = 1 + BI_RLE4 = 2 + BI_BITFIELDS = 3 + BI_JPEG = 4 + BI_PNG = 5 +) + +// SetDIBitsToDevice fuColorUse +const ( + DIB_PAL_COLORS = 1 + DIB_RGB_COLORS = 0 +) + +const ( + STANDARD_RIGHTS_REQUIRED = 0x000F +) + +// Service Control Manager object specific access types +const ( + SC_MANAGER_CONNECT = 0x0001 + SC_MANAGER_CREATE_SERVICE = 0x0002 + SC_MANAGER_ENUMERATE_SERVICE = 0x0004 + SC_MANAGER_LOCK = 0x0008 + SC_MANAGER_QUERY_LOCK_STATUS = 0x0010 + SC_MANAGER_MODIFY_BOOT_CONFIG = 0x0020 + SC_MANAGER_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE | SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_LOCK | SC_MANAGER_QUERY_LOCK_STATUS | SC_MANAGER_MODIFY_BOOT_CONFIG +) + +// Service Types (Bit Mask) +const ( + SERVICE_KERNEL_DRIVER = 0x00000001 + SERVICE_FILE_SYSTEM_DRIVER = 0x00000002 + SERVICE_ADAPTER = 0x00000004 + SERVICE_RECOGNIZER_DRIVER = 0x00000008 + SERVICE_DRIVER = SERVICE_KERNEL_DRIVER | SERVICE_FILE_SYSTEM_DRIVER | SERVICE_RECOGNIZER_DRIVER + SERVICE_WIN32_OWN_PROCESS = 0x00000010 + SERVICE_WIN32_SHARE_PROCESS = 0x00000020 + SERVICE_WIN32 = SERVICE_WIN32_OWN_PROCESS | SERVICE_WIN32_SHARE_PROCESS + SERVICE_INTERACTIVE_PROCESS = 0x00000100 + SERVICE_TYPE_ALL = SERVICE_WIN32 | SERVICE_ADAPTER | SERVICE_DRIVER | SERVICE_INTERACTIVE_PROCESS +) + +// Service State -- for CurrentState +const ( + SERVICE_STOPPED = 0x00000001 + SERVICE_START_PENDING = 0x00000002 + SERVICE_STOP_PENDING = 0x00000003 + SERVICE_RUNNING = 0x00000004 + SERVICE_CONTINUE_PENDING = 0x00000005 + SERVICE_PAUSE_PENDING = 0x00000006 + SERVICE_PAUSED = 0x00000007 +) + +// Controls Accepted (Bit Mask) +const ( + SERVICE_ACCEPT_STOP = 0x00000001 + SERVICE_ACCEPT_PAUSE_CONTINUE = 0x00000002 + SERVICE_ACCEPT_SHUTDOWN = 0x00000004 + SERVICE_ACCEPT_PARAMCHANGE = 0x00000008 + SERVICE_ACCEPT_NETBINDCHANGE = 0x00000010 + SERVICE_ACCEPT_HARDWAREPROFILECHANGE = 0x00000020 + SERVICE_ACCEPT_POWEREVENT = 0x00000040 + SERVICE_ACCEPT_SESSIONCHANGE = 0x00000080 + SERVICE_ACCEPT_PRESHUTDOWN = 0x00000100 + SERVICE_ACCEPT_TIMECHANGE = 0x00000200 + SERVICE_ACCEPT_TRIGGEREVENT = 0x00000400 +) + +// Service object specific access type +const ( + SERVICE_QUERY_CONFIG = 0x0001 + SERVICE_CHANGE_CONFIG = 0x0002 + SERVICE_QUERY_STATUS = 0x0004 + SERVICE_ENUMERATE_DEPENDENTS = 0x0008 + SERVICE_START = 0x0010 + SERVICE_STOP = 0x0020 + SERVICE_PAUSE_CONTINUE = 0x0040 + SERVICE_INTERROGATE = 0x0080 + SERVICE_USER_DEFINED_CONTROL = 0x0100 + + SERVICE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | + SERVICE_QUERY_CONFIG | + SERVICE_CHANGE_CONFIG | + SERVICE_QUERY_STATUS | + SERVICE_ENUMERATE_DEPENDENTS | + SERVICE_START | + SERVICE_STOP | + SERVICE_PAUSE_CONTINUE | + SERVICE_INTERROGATE | + SERVICE_USER_DEFINED_CONTROL +) + +// MapVirtualKey maptypes +const ( + MAPVK_VK_TO_CHAR = 2 + MAPVK_VK_TO_VSC = 0 + MAPVK_VSC_TO_VK = 1 + MAPVK_VSC_TO_VK_EX = 3 +) + +// ReadEventLog Flags +const ( + EVENTLOG_SEEK_READ = 0x0002 + EVENTLOG_SEQUENTIAL_READ = 0x0001 + EVENTLOG_FORWARDS_READ = 0x0004 + EVENTLOG_BACKWARDS_READ = 0x0008 +) + +// CreateToolhelp32Snapshot flags +const ( + TH32CS_SNAPHEAPLIST = 0x00000001 + TH32CS_SNAPPROCESS = 0x00000002 + TH32CS_SNAPTHREAD = 0x00000004 + TH32CS_SNAPMODULE = 0x00000008 + TH32CS_SNAPMODULE32 = 0x00000010 + TH32CS_INHERIT = 0x80000000 + TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPMODULE | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD +) + +const ( + MAX_MODULE_NAME32 = 255 + MAX_PATH = 260 +) + +const ( + FOREGROUND_BLUE = 0x0001 + FOREGROUND_GREEN = 0x0002 + FOREGROUND_RED = 0x0004 + FOREGROUND_INTENSITY = 0x0008 + BACKGROUND_BLUE = 0x0010 + BACKGROUND_GREEN = 0x0020 + BACKGROUND_RED = 0x0040 + BACKGROUND_INTENSITY = 0x0080 + COMMON_LVB_LEADING_BYTE = 0x0100 + COMMON_LVB_TRAILING_BYTE = 0x0200 + COMMON_LVB_GRID_HORIZONTAL = 0x0400 + COMMON_LVB_GRID_LVERTICAL = 0x0800 + COMMON_LVB_GRID_RVERTICAL = 0x1000 + COMMON_LVB_REVERSE_VIDEO = 0x4000 + COMMON_LVB_UNDERSCORE = 0x8000 +) + +// Flags used by the DWM_BLURBEHIND structure to indicate +// which of its members contain valid information. +const ( + DWM_BB_ENABLE = 0x00000001 // A value for the fEnable member has been specified. + DWM_BB_BLURREGION = 0x00000002 // A value for the hRgnBlur member has been specified. + DWM_BB_TRANSITIONONMAXIMIZED = 0x00000004 // A value for the fTransitionOnMaximized member has been specified. +) + +// Flags used by the DwmEnableComposition function +// to change the state of Desktop Window Manager (DWM) composition. +const ( + DWM_EC_DISABLECOMPOSITION = 0 // Disable composition + DWM_EC_ENABLECOMPOSITION = 1 // Enable composition +) + +// enum-lite implementation for the following constant structure +type DWM_SHOWCONTACT int32 + +const ( + DWMSC_DOWN = 0x00000001 + DWMSC_UP = 0x00000002 + DWMSC_DRAG = 0x00000004 + DWMSC_HOLD = 0x00000008 + DWMSC_PENBARREL = 0x00000010 + DWMSC_NONE = 0x00000000 + DWMSC_ALL = 0xFFFFFFFF +) + +// enum-lite implementation for the following constant structure +type DWM_SOURCE_FRAME_SAMPLING int32 + +// TODO: need to verify this construction +// Flags used by the DwmSetPresentParameters function +// to specify the frame sampling type +const ( + DWM_SOURCE_FRAME_SAMPLING_POINT = iota + 1 + DWM_SOURCE_FRAME_SAMPLING_COVERAGE + DWM_SOURCE_FRAME_SAMPLING_LAST +) + +// Flags used by the DWM_THUMBNAIL_PROPERTIES structure to +// indicate which of its members contain valid information. +const ( + DWM_TNP_RECTDESTINATION = 0x00000001 // A value for the rcDestination member has been specified + DWM_TNP_RECTSOURCE = 0x00000002 // A value for the rcSource member has been specified + DWM_TNP_OPACITY = 0x00000004 // A value for the opacity member has been specified + DWM_TNP_VISIBLE = 0x00000008 // A value for the fVisible member has been specified + DWM_TNP_SOURCECLIENTAREAONLY = 0x00000010 // A value for the fSourceClientAreaOnly member has been specified +) + +// enum-lite implementation for the following constant structure +type DWMFLIP3DWINDOWPOLICY int32 + +// TODO: need to verify this construction +// Flags used by the DwmSetWindowAttribute function +// to specify the Flip3D window policy +const ( + DWMFLIP3D_DEFAULT = iota + 1 + DWMFLIP3D_EXCLUDEBELOW + DWMFLIP3D_EXCLUDEABOVE + DWMFLIP3D_LAST +) + +// enum-lite implementation for the following constant structure +type DWMNCRENDERINGPOLICY int32 + +// TODO: need to verify this construction +// Flags used by the DwmSetWindowAttribute function +// to specify the non-client area rendering policy +const ( + DWMNCRP_USEWINDOWSTYLE = iota + 1 + DWMNCRP_DISABLED + DWMNCRP_ENABLED + DWMNCRP_LAST +) + +// enum-lite implementation for the following constant structure +type DWMTRANSITION_OWNEDWINDOW_TARGET int32 + +const ( + DWMTRANSITION_OWNEDWINDOW_NULL = -1 + DWMTRANSITION_OWNEDWINDOW_REPOSITION = 0 +) + +// enum-lite implementation for the following constant structure +type DWMWINDOWATTRIBUTE int32 + +// TODO: need to verify this construction +// Flags used by the DwmGetWindowAttribute and DwmSetWindowAttribute functions +// to specify window attributes for non-client rendering +const ( + DWMWA_NCRENDERING_ENABLED = iota + 1 + DWMWA_NCRENDERING_POLICY + DWMWA_TRANSITIONS_FORCEDISABLED + DWMWA_ALLOW_NCPAINT + DWMWA_CAPTION_BUTTON_BOUNDS + DWMWA_NONCLIENT_RTL_LAYOUT + DWMWA_FORCE_ICONIC_REPRESENTATION + DWMWA_FLIP3D_POLICY + DWMWA_EXTENDED_FRAME_BOUNDS + DWMWA_HAS_ICONIC_BITMAP + DWMWA_DISALLOW_PEEK + DWMWA_EXCLUDED_FROM_PEEK + DWMWA_CLOAK + DWMWA_CLOAKED + DWMWA_FREEZE_REPRESENTATION + DWMWA_LAST +) + +// enum-lite implementation for the following constant structure +type GESTURE_TYPE int32 + +// TODO: use iota? +// Identifies the gesture type +const ( + GT_PEN_TAP = 0 + GT_PEN_DOUBLETAP = 1 + GT_PEN_RIGHTTAP = 2 + GT_PEN_PRESSANDHOLD = 3 + GT_PEN_PRESSANDHOLDABORT = 4 + GT_TOUCH_TAP = 5 + GT_TOUCH_DOUBLETAP = 6 + GT_TOUCH_RIGHTTAP = 7 + GT_TOUCH_PRESSANDHOLD = 8 + GT_TOUCH_PRESSANDHOLDABORT = 9 + GT_TOUCH_PRESSANDTAP = 10 +) + +// Icons +const ( + ICON_SMALL = 0 + ICON_BIG = 1 + ICON_SMALL2 = 2 +) + +const ( + SIZE_RESTORED = 0 + SIZE_MINIMIZED = 1 + SIZE_MAXIMIZED = 2 + SIZE_MAXSHOW = 3 + SIZE_MAXHIDE = 4 +) + +// XButton values +const ( + XBUTTON1 = 1 + XBUTTON2 = 2 +) + +// Devmode +const ( + DM_SPECVERSION = 0x0401 + + DM_ORIENTATION = 0x00000001 + DM_PAPERSIZE = 0x00000002 + DM_PAPERLENGTH = 0x00000004 + DM_PAPERWIDTH = 0x00000008 + DM_SCALE = 0x00000010 + DM_POSITION = 0x00000020 + DM_NUP = 0x00000040 + DM_DISPLAYORIENTATION = 0x00000080 + DM_COPIES = 0x00000100 + DM_DEFAULTSOURCE = 0x00000200 + DM_PRINTQUALITY = 0x00000400 + DM_COLOR = 0x00000800 + DM_DUPLEX = 0x00001000 + DM_YRESOLUTION = 0x00002000 + DM_TTOPTION = 0x00004000 + DM_COLLATE = 0x00008000 + DM_FORMNAME = 0x00010000 + DM_LOGPIXELS = 0x00020000 + DM_BITSPERPEL = 0x00040000 + DM_PELSWIDTH = 0x00080000 + DM_PELSHEIGHT = 0x00100000 + DM_DISPLAYFLAGS = 0x00200000 + DM_DISPLAYFREQUENCY = 0x00400000 + DM_ICMMETHOD = 0x00800000 + DM_ICMINTENT = 0x01000000 + DM_MEDIATYPE = 0x02000000 + DM_DITHERTYPE = 0x04000000 + DM_PANNINGWIDTH = 0x08000000 + DM_PANNINGHEIGHT = 0x10000000 + DM_DISPLAYFIXEDOUTPUT = 0x20000000 +) + +// ChangeDisplaySettings +const ( + CDS_UPDATEREGISTRY = 0x00000001 + CDS_TEST = 0x00000002 + CDS_FULLSCREEN = 0x00000004 + CDS_GLOBAL = 0x00000008 + CDS_SET_PRIMARY = 0x00000010 + CDS_VIDEOPARAMETERS = 0x00000020 + CDS_RESET = 0x40000000 + CDS_NORESET = 0x10000000 + + DISP_CHANGE_SUCCESSFUL = 0 + DISP_CHANGE_RESTART = 1 + DISP_CHANGE_FAILED = -1 + DISP_CHANGE_BADMODE = -2 + DISP_CHANGE_NOTUPDATED = -3 + DISP_CHANGE_BADFLAGS = -4 + DISP_CHANGE_BADPARAM = -5 + DISP_CHANGE_BADDUALVIEW = -6 +) + +const ( + ENUM_CURRENT_SETTINGS = 0xFFFFFFFF + ENUM_REGISTRY_SETTINGS = 0xFFFFFFFE +) + +// PIXELFORMATDESCRIPTOR +const ( + PFD_TYPE_RGBA = 0 + PFD_TYPE_COLORINDEX = 1 + + PFD_MAIN_PLANE = 0 + PFD_OVERLAY_PLANE = 1 + PFD_UNDERLAY_PLANE = -1 + + PFD_DOUBLEBUFFER = 0x00000001 + PFD_STEREO = 0x00000002 + PFD_DRAW_TO_WINDOW = 0x00000004 + PFD_DRAW_TO_BITMAP = 0x00000008 + PFD_SUPPORT_GDI = 0x00000010 + PFD_SUPPORT_OPENGL = 0x00000020 + PFD_GENERIC_FORMAT = 0x00000040 + PFD_NEED_PALETTE = 0x00000080 + PFD_NEED_SYSTEM_PALETTE = 0x00000100 + PFD_SWAP_EXCHANGE = 0x00000200 + PFD_SWAP_COPY = 0x00000400 + PFD_SWAP_LAYER_BUFFERS = 0x00000800 + PFD_GENERIC_ACCELERATED = 0x00001000 + PFD_SUPPORT_DIRECTDRAW = 0x00002000 + PFD_DIRECT3D_ACCELERATED = 0x00004000 + PFD_SUPPORT_COMPOSITION = 0x00008000 + + PFD_DEPTH_DONTCARE = 0x20000000 + PFD_DOUBLEBUFFER_DONTCARE = 0x40000000 + PFD_STEREO_DONTCARE = 0x80000000 +) + +const ( + INPUT_MOUSE = 0 + INPUT_KEYBOARD = 1 + INPUT_HARDWARE = 2 +) + +const ( + MOUSEEVENTF_ABSOLUTE = 0x8000 + MOUSEEVENTF_HWHEEL = 0x01000 + MOUSEEVENTF_MOVE = 0x0001 + MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000 + MOUSEEVENTF_LEFTDOWN = 0x0002 + MOUSEEVENTF_LEFTUP = 0x0004 + MOUSEEVENTF_RIGHTDOWN = 0x0008 + MOUSEEVENTF_RIGHTUP = 0x0010 + MOUSEEVENTF_MIDDLEDOWN = 0x0020 + MOUSEEVENTF_MIDDLEUP = 0x0040 + MOUSEEVENTF_VIRTUALDESK = 0x4000 + MOUSEEVENTF_WHEEL = 0x0800 + MOUSEEVENTF_XDOWN = 0x0080 + MOUSEEVENTF_XUP = 0x0100 +) + +// Windows Hooks (WH_*) +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms644990(v=vs.85).aspx +const ( + WH_CALLWNDPROC = 4 + WH_CALLWNDPROCRET = 12 + WH_CBT = 5 + WH_DEBUG = 9 + WH_FOREGROUNDIDLE = 11 + WH_GETMESSAGE = 3 + WH_JOURNALPLAYBACK = 1 + WH_JOURNALRECORD = 0 + WH_KEYBOARD = 2 + WH_KEYBOARD_LL = 13 + WH_MOUSE = 7 + WH_MOUSE_LL = 14 + WH_MSGFILTER = -1 + WH_SHELL = 10 + WH_SYSMSGFILTER = 6 +) + +// ComboBox return values +const ( + CB_OKAY = 0 + CB_ERR = ^uintptr(0) // -1 + CB_ERRSPACE = ^uintptr(1) // -2 +) + +// ComboBox notifications +const ( + CBN_ERRSPACE = -1 + CBN_SELCHANGE = 1 + CBN_DBLCLK = 2 + CBN_SETFOCUS = 3 + CBN_KILLFOCUS = 4 + CBN_EDITCHANGE = 5 + CBN_EDITUPDATE = 6 + CBN_DROPDOWN = 7 + CBN_CLOSEUP = 8 + CBN_SELENDOK = 9 + CBN_SELENDCANCEL = 10 +) + +// ComboBox styles +const ( + CBS_SIMPLE = 0x0001 + CBS_DROPDOWN = 0x0002 + CBS_DROPDOWNLIST = 0x0003 + CBS_OWNERDRAWFIXED = 0x0010 + CBS_OWNERDRAWVARIABLE = 0x0020 + CBS_AUTOHSCROLL = 0x0040 + CBS_OEMCONVERT = 0x0080 + CBS_SORT = 0x0100 + CBS_HASSTRINGS = 0x0200 + CBS_NOINTEGRALHEIGHT = 0x0400 + CBS_DISABLENOSCROLL = 0x0800 + CBS_UPPERCASE = 0x2000 + CBS_LOWERCASE = 0x4000 +) + +// ComboBox messages +const ( + CB_GETEDITSEL = 0x0140 + CB_LIMITTEXT = 0x0141 + CB_SETEDITSEL = 0x0142 + CB_ADDSTRING = 0x0143 + CB_DELETESTRING = 0x0144 + CB_DIR = 0x0145 + CB_GETCOUNT = 0x0146 + CB_GETCURSEL = 0x0147 + CB_GETLBTEXT = 0x0148 + CB_GETLBTEXTLEN = 0x0149 + CB_INSERTSTRING = 0x014A + CB_RESETCONTENT = 0x014B + CB_FINDSTRING = 0x014C + CB_SELECTSTRING = 0x014D + CB_SETCURSEL = 0x014E + CB_SHOWDROPDOWN = 0x014F + CB_GETITEMDATA = 0x0150 + CB_SETITEMDATA = 0x0151 + CB_GETDROPPEDCONTROLRECT = 0x0152 + CB_SETITEMHEIGHT = 0x0153 + CB_GETITEMHEIGHT = 0x0154 + CB_SETEXTENDEDUI = 0x0155 + CB_GETEXTENDEDUI = 0x0156 + CB_GETDROPPEDSTATE = 0x0157 + CB_FINDSTRINGEXACT = 0x0158 + CB_SETLOCALE = 0x0159 + CB_GETLOCALE = 0x015A + CB_GETTOPINDEX = 0x015b + CB_SETTOPINDEX = 0x015c + CB_GETHORIZONTALEXTENT = 0x015d + CB_SETHORIZONTALEXTENT = 0x015e + CB_GETDROPPEDWIDTH = 0x015f + CB_SETDROPPEDWIDTH = 0x0160 + CB_INITSTORAGE = 0x0161 + CB_MULTIPLEADDSTRING = 0x0163 + CB_GETCOMBOBOXINFO = 0x0164 +) + +// TreeView styles +const ( + TVS_HASBUTTONS = 0x0001 + TVS_HASLINES = 0x0002 + TVS_LINESATROOT = 0x0004 + TVS_EDITLABELS = 0x0008 + TVS_DISABLEDRAGDROP = 0x0010 + TVS_SHOWSELALWAYS = 0x0020 + TVS_RTLREADING = 0x0040 + TVS_NOTOOLTIPS = 0x0080 + TVS_CHECKBOXES = 0x0100 + TVS_TRACKSELECT = 0x0200 + TVS_SINGLEEXPAND = 0x0400 + TVS_INFOTIP = 0x0800 + TVS_FULLROWSELECT = 0x1000 + TVS_NOSCROLL = 0x2000 + TVS_NONEVENHEIGHT = 0x4000 + TVS_NOHSCROLL = 0x8000 +) + +const ( + TVS_EX_NOSINGLECOLLAPSE = 0x0001 + TVS_EX_MULTISELECT = 0x0002 + TVS_EX_DOUBLEBUFFER = 0x0004 + TVS_EX_NOINDENTSTATE = 0x0008 + TVS_EX_RICHTOOLTIP = 0x0010 + TVS_EX_AUTOHSCROLL = 0x0020 + TVS_EX_FADEINOUTEXPANDOS = 0x0040 + TVS_EX_PARTIALCHECKBOXES = 0x0080 + TVS_EX_EXCLUSIONCHECKBOXES = 0x0100 + TVS_EX_DIMMEDCHECKBOXES = 0x0200 + TVS_EX_DRAWIMAGEASYNC = 0x0400 +) + +const ( + TVIF_TEXT = 0x0001 + TVIF_IMAGE = 0x0002 + TVIF_PARAM = 0x0004 + TVIF_STATE = 0x0008 + TVIF_HANDLE = 0x0010 + TVIF_SELECTEDIMAGE = 0x0020 + TVIF_CHILDREN = 0x0040 + TVIF_INTEGRAL = 0x0080 + TVIF_STATEEX = 0x0100 + TVIF_EXPANDEDIMAGE = 0x0200 +) + +const ( + TVIS_SELECTED = 0x0002 + TVIS_CUT = 0x0004 + TVIS_DROPHILITED = 0x0008 + TVIS_BOLD = 0x0010 + TVIS_EXPANDED = 0x0020 + TVIS_EXPANDEDONCE = 0x0040 + TVIS_EXPANDPARTIAL = 0x0080 + TVIS_OVERLAYMASK = 0x0F00 + TVIS_STATEIMAGEMASK = 0xF000 + TVIS_USERMASK = 0xF000 +) + +const ( + TVIS_EX_FLAT = 0x0001 + TVIS_EX_DISABLED = 0x0002 + TVIS_EX_ALL = 0x0002 +) + +const ( + TVI_ROOT = ^HTREEITEM(0xffff) + TVI_FIRST = ^HTREEITEM(0xfffe) + TVI_LAST = ^HTREEITEM(0xfffd) + TVI_SORT = ^HTREEITEM(0xfffc) +) + +// TVM_EXPAND action flags +const ( + TVE_COLLAPSE = 0x0001 + TVE_EXPAND = 0x0002 + TVE_TOGGLE = 0x0003 + TVE_EXPANDPARTIAL = 0x4000 + TVE_COLLAPSERESET = 0x8000 +) + +const ( + TVGN_CARET = 9 +) + +// TreeView messages +const ( + TV_FIRST = 0x1100 + + TVM_INSERTITEM = TV_FIRST + 50 + TVM_DELETEITEM = TV_FIRST + 1 + TVM_EXPAND = TV_FIRST + 2 + TVM_GETITEMRECT = TV_FIRST + 4 + TVM_GETCOUNT = TV_FIRST + 5 + TVM_GETINDENT = TV_FIRST + 6 + TVM_SETINDENT = TV_FIRST + 7 + TVM_GETIMAGELIST = TV_FIRST + 8 + TVM_SETIMAGELIST = TV_FIRST + 9 + TVM_GETNEXTITEM = TV_FIRST + 10 + TVM_SELECTITEM = TV_FIRST + 11 + TVM_GETITEM = TV_FIRST + 62 + TVM_SETITEM = TV_FIRST + 63 + TVM_EDITLABEL = TV_FIRST + 65 + TVM_GETEDITCONTROL = TV_FIRST + 15 + TVM_GETVISIBLECOUNT = TV_FIRST + 16 + TVM_HITTEST = TV_FIRST + 17 + TVM_CREATEDRAGIMAGE = TV_FIRST + 18 + TVM_SORTCHILDREN = TV_FIRST + 19 + TVM_ENSUREVISIBLE = TV_FIRST + 20 + TVM_SORTCHILDRENCB = TV_FIRST + 21 + TVM_ENDEDITLABELNOW = TV_FIRST + 22 + TVM_GETISEARCHSTRING = TV_FIRST + 64 + TVM_SETTOOLTIPS = TV_FIRST + 24 + TVM_GETTOOLTIPS = TV_FIRST + 25 + TVM_SETINSERTMARK = TV_FIRST + 26 + TVM_SETUNICODEFORMAT = CCM_SETUNICODEFORMAT + TVM_GETUNICODEFORMAT = CCM_GETUNICODEFORMAT + TVM_SETITEMHEIGHT = TV_FIRST + 27 + TVM_GETITEMHEIGHT = TV_FIRST + 28 + TVM_SETBKCOLOR = TV_FIRST + 29 + TVM_SETTEXTCOLOR = TV_FIRST + 30 + TVM_GETBKCOLOR = TV_FIRST + 31 + TVM_GETTEXTCOLOR = TV_FIRST + 32 + TVM_SETSCROLLTIME = TV_FIRST + 33 + TVM_GETSCROLLTIME = TV_FIRST + 34 + TVM_SETINSERTMARKCOLOR = TV_FIRST + 37 + TVM_GETINSERTMARKCOLOR = TV_FIRST + 38 + TVM_GETITEMSTATE = TV_FIRST + 39 + TVM_SETLINECOLOR = TV_FIRST + 40 + TVM_GETLINECOLOR = TV_FIRST + 41 + TVM_MAPACCIDTOHTREEITEM = TV_FIRST + 42 + TVM_MAPHTREEITEMTOACCID = TV_FIRST + 43 + TVM_SETEXTENDEDSTYLE = TV_FIRST + 44 + TVM_GETEXTENDEDSTYLE = TV_FIRST + 45 + TVM_SETAUTOSCROLLINFO = TV_FIRST + 59 +) + +// TreeView notifications +const ( + TVN_FIRST = ^uint32(399) + + TVN_SELCHANGING = TVN_FIRST - 50 + TVN_SELCHANGED = TVN_FIRST - 51 + TVN_GETDISPINFO = TVN_FIRST - 52 + TVN_ITEMEXPANDING = TVN_FIRST - 54 + TVN_ITEMEXPANDED = TVN_FIRST - 55 + TVN_BEGINDRAG = TVN_FIRST - 56 + TVN_BEGINRDRAG = TVN_FIRST - 57 + TVN_DELETEITEM = TVN_FIRST - 58 + TVN_BEGINLABELEDIT = TVN_FIRST - 59 + TVN_ENDLABELEDIT = TVN_FIRST - 60 + TVN_KEYDOWN = TVN_FIRST - 12 + TVN_GETINFOTIP = TVN_FIRST - 14 + TVN_SINGLEEXPAND = TVN_FIRST - 15 + TVN_ITEMCHANGING = TVN_FIRST - 17 + TVN_ITEMCHANGED = TVN_FIRST - 19 + TVN_ASYNCDRAW = TVN_FIRST - 20 +) + +// TreeView hit test constants +const ( + TVHT_NOWHERE = 1 + TVHT_ONITEMICON = 2 + TVHT_ONITEMLABEL = 4 + TVHT_ONITEM = TVHT_ONITEMICON | TVHT_ONITEMLABEL | TVHT_ONITEMSTATEICON + TVHT_ONITEMINDENT = 8 + TVHT_ONITEMBUTTON = 16 + TVHT_ONITEMRIGHT = 32 + TVHT_ONITEMSTATEICON = 64 + TVHT_ABOVE = 256 + TVHT_BELOW = 512 + TVHT_TORIGHT = 1024 + TVHT_TOLEFT = 2048 +) + +type HTREEITEM HANDLE + +type TVITEM struct { + Mask uint32 + HItem HTREEITEM + State uint32 + StateMask uint32 + PszText uintptr + CchTextMax int32 + IImage int32 + ISelectedImage int32 + CChildren int32 + LParam uintptr +} + +/*type TVITEMEX struct { + mask UINT + hItem HTREEITEM + state UINT + stateMask UINT + pszText LPWSTR + cchTextMax int + iImage int + iSelectedImage int + cChildren int + lParam LPARAM + iIntegral int + uStateEx UINT + hwnd HWND + iExpandedImage int +}*/ + +type TVINSERTSTRUCT struct { + HParent HTREEITEM + HInsertAfter HTREEITEM + Item TVITEM + // itemex TVITEMEX +} + +type NMTREEVIEW struct { + Hdr NMHDR + Action uint32 + ItemOld TVITEM + ItemNew TVITEM + PtDrag POINT +} + +type NMTVDISPINFO struct { + Hdr NMHDR + Item TVITEM +} + +type NMTVKEYDOWN struct { + Hdr NMHDR + WVKey uint16 + Flags uint32 +} + +type TVHITTESTINFO struct { + Pt POINT + Flags uint32 + HItem HTREEITEM +} + +// TabPage support + +const TCM_FIRST = 0x1300 +const TCN_FIRST = -550 + +const ( + TCS_SCROLLOPPOSITE = 0x0001 + TCS_BOTTOM = 0x0002 + TCS_RIGHT = 0x0002 + TCS_MULTISELECT = 0x0004 + TCS_FLATBUTTONS = 0x0008 + TCS_FORCEICONLEFT = 0x0010 + TCS_FORCELABELLEFT = 0x0020 + TCS_HOTTRACK = 0x0040 + TCS_VERTICAL = 0x0080 + TCS_TABS = 0x0000 + TCS_BUTTONS = 0x0100 + TCS_SINGLELINE = 0x0000 + TCS_MULTILINE = 0x0200 + TCS_RIGHTJUSTIFY = 0x0000 + TCS_FIXEDWIDTH = 0x0400 + TCS_RAGGEDRIGHT = 0x0800 + TCS_FOCUSONBUTTONDOWN = 0x1000 + TCS_OWNERDRAWFIXED = 0x2000 + TCS_TOOLTIPS = 0x4000 + TCS_FOCUSNEVER = 0x8000 +) + +const ( + TCS_EX_FLATSEPARATORS = 0x00000001 + TCS_EX_REGISTERDROP = 0x00000002 +) + +const ( + TCM_GETIMAGELIST = TCM_FIRST + 2 + TCM_SETIMAGELIST = TCM_FIRST + 3 + TCM_GETITEMCOUNT = TCM_FIRST + 4 + TCM_GETITEM = TCM_FIRST + 60 + TCM_SETITEM = TCM_FIRST + 61 + TCM_INSERTITEM = TCM_FIRST + 62 + TCM_DELETEITEM = TCM_FIRST + 8 + TCM_DELETEALLITEMS = TCM_FIRST + 9 + TCM_GETITEMRECT = TCM_FIRST + 10 + TCM_GETCURSEL = TCM_FIRST + 11 + TCM_SETCURSEL = TCM_FIRST + 12 + TCM_HITTEST = TCM_FIRST + 13 + TCM_SETITEMEXTRA = TCM_FIRST + 14 + TCM_ADJUSTRECT = TCM_FIRST + 40 + TCM_SETITEMSIZE = TCM_FIRST + 41 + TCM_REMOVEIMAGE = TCM_FIRST + 42 + TCM_SETPADDING = TCM_FIRST + 43 + TCM_GETROWCOUNT = TCM_FIRST + 44 + TCM_GETTOOLTIPS = TCM_FIRST + 45 + TCM_SETTOOLTIPS = TCM_FIRST + 46 + TCM_GETCURFOCUS = TCM_FIRST + 47 + TCM_SETCURFOCUS = TCM_FIRST + 48 + TCM_SETMINTABWIDTH = TCM_FIRST + 49 + TCM_DESELECTALL = TCM_FIRST + 50 + TCM_HIGHLIGHTITEM = TCM_FIRST + 51 + TCM_SETEXTENDEDSTYLE = TCM_FIRST + 52 + TCM_GETEXTENDEDSTYLE = TCM_FIRST + 53 + TCM_SETUNICODEFORMAT = CCM_SETUNICODEFORMAT + TCM_GETUNICODEFORMAT = CCM_GETUNICODEFORMAT +) + +const ( + TCIF_TEXT = 0x0001 + TCIF_IMAGE = 0x0002 + TCIF_RTLREADING = 0x0004 + TCIF_PARAM = 0x0008 + TCIF_STATE = 0x0010 +) + +const ( + TCIS_BUTTONPRESSED = 0x0001 + TCIS_HIGHLIGHTED = 0x0002 +) + +const ( + TCHT_NOWHERE = 0x0001 + TCHT_ONITEMICON = 0x0002 + TCHT_ONITEMLABEL = 0x0004 + TCHT_ONITEM = TCHT_ONITEMICON | TCHT_ONITEMLABEL +) + +const ( + TCN_KEYDOWN = TCN_FIRST - 0 + TCN_SELCHANGE = TCN_FIRST - 1 + TCN_SELCHANGING = TCN_FIRST - 2 + TCN_GETOBJECT = TCN_FIRST - 3 + TCN_FOCUSCHANGE = TCN_FIRST - 4 +) + +type TCITEMHEADER struct { + Mask uint32 + LpReserved1 uint32 + LpReserved2 uint32 + PszText *uint16 + CchTextMax int32 + IImage int32 +} + +type TCITEM struct { + Mask uint32 + DwState uint32 + DwStateMask uint32 + PszText *uint16 + CchTextMax int32 + IImage int32 + LParam uintptr +} + +type TCHITTESTINFO struct { + Pt POINT + flags uint32 +} + +type NMTCKEYDOWN struct { + Hdr NMHDR + WVKey uint16 + Flags uint32 +} + +// Menu support constants + +// Constants for MENUITEMINFO.fMask +const ( + MIIM_STATE = 1 + MIIM_ID = 2 + MIIM_SUBMENU = 4 + MIIM_CHECKMARKS = 8 + MIIM_TYPE = 16 + MIIM_DATA = 32 + MIIM_STRING = 64 + MIIM_BITMAP = 128 + MIIM_FTYPE = 256 +) + +// Constants for MENUITEMINFO.fType +const ( + MFT_BITMAP = 4 + MFT_MENUBARBREAK = 32 + MFT_MENUBREAK = 64 + MFT_OWNERDRAW = 256 + MFT_RADIOCHECK = 512 + MFT_RIGHTJUSTIFY = 0x4000 + MFT_SEPARATOR = 0x800 + MFT_RIGHTORDER = 0x2000 + MFT_STRING = 0 +) + +// Constants for MENUITEMINFO.fState +const ( + MFS_CHECKED = 8 + MFS_DEFAULT = 4096 + MFS_DISABLED = 3 + MFS_ENABLED = 0 + MFS_GRAYED = 3 + MFS_HILITE = 128 + MFS_UNCHECKED = 0 + MFS_UNHILITE = 0 +) + +// Constants for MENUITEMINFO.hbmp* +const ( + HBMMENU_CALLBACK = -1 + HBMMENU_SYSTEM = 1 + HBMMENU_MBAR_RESTORE = 2 + HBMMENU_MBAR_MINIMIZE = 3 + HBMMENU_MBAR_CLOSE = 5 + HBMMENU_MBAR_CLOSE_D = 6 + HBMMENU_MBAR_MINIMIZE_D = 7 + HBMMENU_POPUP_CLOSE = 8 + HBMMENU_POPUP_RESTORE = 9 + HBMMENU_POPUP_MAXIMIZE = 10 + HBMMENU_POPUP_MINIMIZE = 11 +) + +// MENUINFO mask constants +const ( + MIM_APPLYTOSUBMENUS = 0x80000000 + MIM_BACKGROUND = 0x00000002 + MIM_HELPID = 0x00000004 + MIM_MAXHEIGHT = 0x00000001 + MIM_MENUDATA = 0x00000008 + MIM_STYLE = 0x00000010 +) + +// MENUINFO style constants +const ( + MNS_AUTODISMISS = 0x10000000 + MNS_CHECKORBMP = 0x04000000 + MNS_DRAGDROP = 0x20000000 + MNS_MODELESS = 0x40000000 + MNS_NOCHECK = 0x80000000 + MNS_NOTIFYBYPOS = 0x08000000 +) + +const ( + MF_BYCOMMAND = 0x00000000 + MF_BYPOSITION = 0x00000400 +) + +type MENUITEMINFO struct { + CbSize uint32 + FMask uint32 + FType uint32 + FState uint32 + WID uint32 + HSubMenu HMENU + HbmpChecked HBITMAP + HbmpUnchecked HBITMAP + DwItemData uintptr + DwTypeData *uint16 + Cch uint32 + HbmpItem HBITMAP +} + +type MENUINFO struct { + CbSize uint32 + FMask uint32 + DwStyle uint32 + CyMax uint32 + HbrBack HBRUSH + DwContextHelpID uint32 + DwMenuData uintptr +} + +// UI state constants +const ( + UIS_SET = 1 + UIS_CLEAR = 2 + UIS_INITIALIZE = 3 +) + +// UI state constants +const ( + UISF_HIDEFOCUS = 0x1 + UISF_HIDEACCEL = 0x2 + UISF_ACTIVE = 0x4 +) + +// Virtual key codes +const ( + VK_LBUTTON = 1 + VK_RBUTTON = 2 + VK_CANCEL = 3 + VK_MBUTTON = 4 + VK_XBUTTON1 = 5 + VK_XBUTTON2 = 6 + VK_BACK = 8 + VK_TAB = 9 + VK_CLEAR = 12 + VK_RETURN = 13 + VK_SHIFT = 16 + VK_CONTROL = 17 + VK_MENU = 18 + VK_PAUSE = 19 + VK_CAPITAL = 20 + VK_KANA = 0x15 + VK_HANGEUL = 0x15 + VK_HANGUL = 0x15 + VK_JUNJA = 0x17 + VK_FINAL = 0x18 + VK_HANJA = 0x19 + VK_KANJI = 0x19 + VK_ESCAPE = 0x1B + VK_CONVERT = 0x1C + VK_NONCONVERT = 0x1D + VK_ACCEPT = 0x1E + VK_MODECHANGE = 0x1F + VK_SPACE = 32 + VK_PRIOR = 33 + VK_NEXT = 34 + VK_END = 35 + VK_HOME = 36 + VK_LEFT = 37 + VK_UP = 38 + VK_RIGHT = 39 + VK_DOWN = 40 + VK_SELECT = 41 + VK_PRINT = 42 + VK_EXECUTE = 43 + VK_SNAPSHOT = 44 + VK_INSERT = 45 + VK_DELETE = 46 + VK_HELP = 47 + VK_LWIN = 0x5B + VK_RWIN = 0x5C + VK_APPS = 0x5D + VK_SLEEP = 0x5F + VK_NUMPAD0 = 0x60 + VK_NUMPAD1 = 0x61 + VK_NUMPAD2 = 0x62 + VK_NUMPAD3 = 0x63 + VK_NUMPAD4 = 0x64 + VK_NUMPAD5 = 0x65 + VK_NUMPAD6 = 0x66 + VK_NUMPAD7 = 0x67 + VK_NUMPAD8 = 0x68 + VK_NUMPAD9 = 0x69 + VK_MULTIPLY = 0x6A + VK_ADD = 0x6B + VK_SEPARATOR = 0x6C + VK_SUBTRACT = 0x6D + VK_DECIMAL = 0x6E + VK_DIVIDE = 0x6F + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + VK_F13 = 0x7C + VK_F14 = 0x7D + VK_F15 = 0x7E + VK_F16 = 0x7F + VK_F17 = 0x80 + VK_F18 = 0x81 + VK_F19 = 0x82 + VK_F20 = 0x83 + VK_F21 = 0x84 + VK_F22 = 0x85 + VK_F23 = 0x86 + VK_F24 = 0x87 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_LSHIFT = 0xA0 + VK_RSHIFT = 0xA1 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + VK_LMENU = 0xA4 + VK_RMENU = 0xA5 + VK_BROWSER_BACK = 0xA6 + VK_BROWSER_FORWARD = 0xA7 + VK_BROWSER_REFRESH = 0xA8 + VK_BROWSER_STOP = 0xA9 + VK_BROWSER_SEARCH = 0xAA + VK_BROWSER_FAVORITES = 0xAB + VK_BROWSER_HOME = 0xAC + VK_VOLUME_MUTE = 0xAD + VK_VOLUME_DOWN = 0xAE + VK_VOLUME_UP = 0xAF + VK_MEDIA_NEXT_TRACK = 0xB0 + VK_MEDIA_PREV_TRACK = 0xB1 + VK_MEDIA_STOP = 0xB2 + VK_MEDIA_PLAY_PAUSE = 0xB3 + VK_LAUNCH_MAIL = 0xB4 + VK_LAUNCH_MEDIA_SELECT = 0xB5 + VK_LAUNCH_APP1 = 0xB6 + VK_LAUNCH_APP2 = 0xB7 + VK_OEM_1 = 0xBA + VK_OEM_PLUS = 0xBB + VK_OEM_COMMA = 0xBC + VK_OEM_MINUS = 0xBD + VK_OEM_PERIOD = 0xBE + VK_OEM_2 = 0xBF + VK_OEM_3 = 0xC0 + VK_OEM_4 = 0xDB + VK_OEM_5 = 0xDC + VK_OEM_6 = 0xDD + VK_OEM_7 = 0xDE + VK_OEM_8 = 0xDF + VK_OEM_102 = 0xE2 + VK_PROCESSKEY = 0xE5 + VK_PACKET = 0xE7 + VK_ATTN = 0xF6 + VK_CRSEL = 0xF7 + VK_EXSEL = 0xF8 + VK_EREOF = 0xF9 + VK_PLAY = 0xFA + VK_ZOOM = 0xFB + VK_NONAME = 0xFC + VK_PA1 = 0xFD + VK_OEM_CLEAR = 0xFE +) + +// ScrollBar constants +const ( + SB_HORZ = 0 + SB_VERT = 1 + SB_CTL = 2 + SB_BOTH = 3 +) + +// ScrollBar commands +const ( + SB_LINEUP = 0 + SB_LINELEFT = 0 + SB_LINEDOWN = 1 + SB_LINERIGHT = 1 + SB_PAGEUP = 2 + SB_PAGELEFT = 2 + SB_PAGEDOWN = 3 + SB_PAGERIGHT = 3 + SB_THUMBPOSITION = 4 + SB_THUMBTRACK = 5 + SB_TOP = 6 + SB_LEFT = 6 + SB_BOTTOM = 7 + SB_RIGHT = 7 + SB_ENDSCROLL = 8 +) + +// [Get|Set]ScrollInfo mask constants +const ( + SIF_RANGE = 1 + SIF_PAGE = 2 + SIF_POS = 4 + SIF_DISABLENOSCROLL = 8 + SIF_TRACKPOS = 16 + SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS +) diff --git a/v2/internal/frontend/desktop/windows/winc/w32/dwmapi.go b/v2/internal/frontend/desktop/windows/winc/w32/dwmapi.go new file mode 100644 index 000000000..f5c1b7559 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/dwmapi.go @@ -0,0 +1,20 @@ +//go:build windows + +package w32 + +import "syscall" + +var ( + moddwmapi = syscall.NewLazyDLL("dwmapi.dll") + + procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") +) + +func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute LPCVOID, cbAttribute uint32) HRESULT { + ret, _, _ := procDwmSetWindowAttribute.Call( + hwnd, + uintptr(dwAttribute), + uintptr(pvAttribute), + uintptr(cbAttribute)) + return HRESULT(ret) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/gdi32.go b/v2/internal/frontend/desktop/windows/winc/w32/gdi32.go new file mode 100644 index 000000000..b4b9053e6 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/gdi32.go @@ -0,0 +1,526 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modgdi32 = syscall.NewLazyDLL("gdi32.dll") + + procGetDeviceCaps = modgdi32.NewProc("GetDeviceCaps") + procDeleteObject = modgdi32.NewProc("DeleteObject") + procCreateFontIndirect = modgdi32.NewProc("CreateFontIndirectW") + procAbortDoc = modgdi32.NewProc("AbortDoc") + procBitBlt = modgdi32.NewProc("BitBlt") + procPatBlt = modgdi32.NewProc("PatBlt") + procCloseEnhMetaFile = modgdi32.NewProc("CloseEnhMetaFile") + procCopyEnhMetaFile = modgdi32.NewProc("CopyEnhMetaFileW") + procCreateBrushIndirect = modgdi32.NewProc("CreateBrushIndirect") + procCreateCompatibleDC = modgdi32.NewProc("CreateCompatibleDC") + procCreateDC = modgdi32.NewProc("CreateDCW") + procCreateDIBSection = modgdi32.NewProc("CreateDIBSection") + procCreateEnhMetaFile = modgdi32.NewProc("CreateEnhMetaFileW") + procCreateIC = modgdi32.NewProc("CreateICW") + procDeleteDC = modgdi32.NewProc("DeleteDC") + procDeleteEnhMetaFile = modgdi32.NewProc("DeleteEnhMetaFile") + procEllipse = modgdi32.NewProc("Ellipse") + procEndDoc = modgdi32.NewProc("EndDoc") + procEndPage = modgdi32.NewProc("EndPage") + procExtCreatePen = modgdi32.NewProc("ExtCreatePen") + procGetEnhMetaFile = modgdi32.NewProc("GetEnhMetaFileW") + procGetEnhMetaFileHeader = modgdi32.NewProc("GetEnhMetaFileHeader") + procGetObject = modgdi32.NewProc("GetObjectW") + procGetStockObject = modgdi32.NewProc("GetStockObject") + procGetTextExtentExPoint = modgdi32.NewProc("GetTextExtentExPointW") + procGetTextExtentPoint32 = modgdi32.NewProc("GetTextExtentPoint32W") + procGetTextMetrics = modgdi32.NewProc("GetTextMetricsW") + procLineTo = modgdi32.NewProc("LineTo") + procMoveToEx = modgdi32.NewProc("MoveToEx") + procPlayEnhMetaFile = modgdi32.NewProc("PlayEnhMetaFile") + procRectangle = modgdi32.NewProc("Rectangle") + procResetDC = modgdi32.NewProc("ResetDCW") + procSelectObject = modgdi32.NewProc("SelectObject") + procSetBkMode = modgdi32.NewProc("SetBkMode") + procSetBrushOrgEx = modgdi32.NewProc("SetBrushOrgEx") + procSetStretchBltMode = modgdi32.NewProc("SetStretchBltMode") + procSetTextColor = modgdi32.NewProc("SetTextColor") + procSetBkColor = modgdi32.NewProc("SetBkColor") + procStartDoc = modgdi32.NewProc("StartDocW") + procStartPage = modgdi32.NewProc("StartPage") + procStretchBlt = modgdi32.NewProc("StretchBlt") + procSetDIBitsToDevice = modgdi32.NewProc("SetDIBitsToDevice") + procChoosePixelFormat = modgdi32.NewProc("ChoosePixelFormat") + procDescribePixelFormat = modgdi32.NewProc("DescribePixelFormat") + procGetEnhMetaFilePixelFormat = modgdi32.NewProc("GetEnhMetaFilePixelFormat") + procGetPixelFormat = modgdi32.NewProc("GetPixelFormat") + procSetPixelFormat = modgdi32.NewProc("SetPixelFormat") + procSwapBuffers = modgdi32.NewProc("SwapBuffers") +) + +func GetDeviceCaps(hdc HDC, index int) int { + ret, _, _ := procGetDeviceCaps.Call( + uintptr(hdc), + uintptr(index)) + + return int(ret) +} + +func DeleteObject(hObject HGDIOBJ) bool { + ret, _, _ := procDeleteObject.Call( + uintptr(hObject)) + + return ret != 0 +} + +func CreateFontIndirect(logFont *LOGFONT) HFONT { + ret, _, _ := procCreateFontIndirect.Call( + uintptr(unsafe.Pointer(logFont))) + + return HFONT(ret) +} + +func AbortDoc(hdc HDC) int { + ret, _, _ := procAbortDoc.Call( + uintptr(hdc)) + + return int(ret) +} + +func BitBlt(hdcDest HDC, nXDest, nYDest, nWidth, nHeight int, hdcSrc HDC, nXSrc, nYSrc int, dwRop uint) { + ret, _, _ := procBitBlt.Call( + uintptr(hdcDest), + uintptr(nXDest), + uintptr(nYDest), + uintptr(nWidth), + uintptr(nHeight), + uintptr(hdcSrc), + uintptr(nXSrc), + uintptr(nYSrc), + uintptr(dwRop)) + + if ret == 0 { + panic("BitBlt failed") + } +} + +func PatBlt(hdc HDC, nXLeft, nYLeft, nWidth, nHeight int, dwRop uint) { + ret, _, _ := procPatBlt.Call( + uintptr(hdc), + uintptr(nXLeft), + uintptr(nYLeft), + uintptr(nWidth), + uintptr(nHeight), + uintptr(dwRop)) + + if ret == 0 { + panic("PatBlt failed") + } +} + +func CloseEnhMetaFile(hdc HDC) HENHMETAFILE { + ret, _, _ := procCloseEnhMetaFile.Call( + uintptr(hdc)) + + return HENHMETAFILE(ret) +} + +func CopyEnhMetaFile(hemfSrc HENHMETAFILE, lpszFile *uint16) HENHMETAFILE { + ret, _, _ := procCopyEnhMetaFile.Call( + uintptr(hemfSrc), + uintptr(unsafe.Pointer(lpszFile))) + + return HENHMETAFILE(ret) +} + +func CreateBrushIndirect(lplb *LOGBRUSH) HBRUSH { + ret, _, _ := procCreateBrushIndirect.Call( + uintptr(unsafe.Pointer(lplb))) + + return HBRUSH(ret) +} + +func CreateCompatibleDC(hdc HDC) HDC { + ret, _, _ := procCreateCompatibleDC.Call( + uintptr(hdc)) + + if ret == 0 { + panic("Create compatible DC failed") + } + + return HDC(ret) +} + +func CreateDC(lpszDriver, lpszDevice, lpszOutput *uint16, lpInitData *DEVMODE) HDC { + ret, _, _ := procCreateDC.Call( + uintptr(unsafe.Pointer(lpszDriver)), + uintptr(unsafe.Pointer(lpszDevice)), + uintptr(unsafe.Pointer(lpszOutput)), + uintptr(unsafe.Pointer(lpInitData))) + + return HDC(ret) +} + +func CreateDIBSection(hdc HDC, pbmi *BITMAPINFO, iUsage uint, ppvBits *unsafe.Pointer, hSection HANDLE, dwOffset uint) HBITMAP { + ret, _, _ := procCreateDIBSection.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(pbmi)), + uintptr(iUsage), + uintptr(unsafe.Pointer(ppvBits)), + uintptr(hSection), + uintptr(dwOffset)) + + return HBITMAP(ret) +} + +func CreateEnhMetaFile(hdcRef HDC, lpFilename *uint16, lpRect *RECT, lpDescription *uint16) HDC { + ret, _, _ := procCreateEnhMetaFile.Call( + uintptr(hdcRef), + uintptr(unsafe.Pointer(lpFilename)), + uintptr(unsafe.Pointer(lpRect)), + uintptr(unsafe.Pointer(lpDescription))) + + return HDC(ret) +} + +func CreateIC(lpszDriver, lpszDevice, lpszOutput *uint16, lpdvmInit *DEVMODE) HDC { + ret, _, _ := procCreateIC.Call( + uintptr(unsafe.Pointer(lpszDriver)), + uintptr(unsafe.Pointer(lpszDevice)), + uintptr(unsafe.Pointer(lpszOutput)), + uintptr(unsafe.Pointer(lpdvmInit))) + + return HDC(ret) +} + +func DeleteDC(hdc HDC) bool { + ret, _, _ := procDeleteDC.Call( + uintptr(hdc)) + + return ret != 0 +} + +func DeleteEnhMetaFile(hemf HENHMETAFILE) bool { + ret, _, _ := procDeleteEnhMetaFile.Call( + uintptr(hemf)) + + return ret != 0 +} + +func Ellipse(hdc HDC, nLeftRect, nTopRect, nRightRect, nBottomRect int32) bool { + ret, _, _ := procEllipse.Call( + uintptr(hdc), + uintptr(nLeftRect), + uintptr(nTopRect), + uintptr(nRightRect), + uintptr(nBottomRect)) + + return ret != 0 +} + +func EndDoc(hdc HDC) int { + ret, _, _ := procEndDoc.Call( + uintptr(hdc)) + + return int(ret) +} + +func EndPage(hdc HDC) int { + ret, _, _ := procEndPage.Call( + uintptr(hdc)) + + return int(ret) +} + +func ExtCreatePen(dwPenStyle, dwWidth uint, lplb *LOGBRUSH, dwStyleCount uint, lpStyle *uint) HPEN { + ret, _, _ := procExtCreatePen.Call( + uintptr(dwPenStyle), + uintptr(dwWidth), + uintptr(unsafe.Pointer(lplb)), + uintptr(dwStyleCount), + uintptr(unsafe.Pointer(lpStyle))) + + return HPEN(ret) +} + +func GetEnhMetaFile(lpszMetaFile *uint16) HENHMETAFILE { + ret, _, _ := procGetEnhMetaFile.Call( + uintptr(unsafe.Pointer(lpszMetaFile))) + + return HENHMETAFILE(ret) +} + +func GetEnhMetaFileHeader(hemf HENHMETAFILE, cbBuffer uint, lpemh *ENHMETAHEADER) uint { + ret, _, _ := procGetEnhMetaFileHeader.Call( + uintptr(hemf), + uintptr(cbBuffer), + uintptr(unsafe.Pointer(lpemh))) + + return uint(ret) +} + +func GetObject(hgdiobj HGDIOBJ, cbBuffer uintptr, lpvObject unsafe.Pointer) int { + ret, _, _ := procGetObject.Call( + uintptr(hgdiobj), + uintptr(cbBuffer), + uintptr(lpvObject)) + + return int(ret) +} + +func GetStockObject(fnObject int) HGDIOBJ { + ret, _, _ := procGetDeviceCaps.Call( + uintptr(fnObject)) + + return HGDIOBJ(ret) +} + +func GetTextExtentExPoint(hdc HDC, lpszStr *uint16, cchString, nMaxExtent int, lpnFit, alpDx *int, lpSize *SIZE) bool { + ret, _, _ := procGetTextExtentExPoint.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(lpszStr)), + uintptr(cchString), + uintptr(nMaxExtent), + uintptr(unsafe.Pointer(lpnFit)), + uintptr(unsafe.Pointer(alpDx)), + uintptr(unsafe.Pointer(lpSize))) + + return ret != 0 +} + +func GetTextExtentPoint32(hdc HDC, lpString *uint16, c int, lpSize *SIZE) bool { + ret, _, _ := procGetTextExtentPoint32.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(lpString)), + uintptr(c), + uintptr(unsafe.Pointer(lpSize))) + + return ret != 0 +} + +func GetTextMetrics(hdc HDC, lptm *TEXTMETRIC) bool { + ret, _, _ := procGetTextMetrics.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(lptm))) + + return ret != 0 +} + +func LineTo(hdc HDC, nXEnd, nYEnd int32) bool { + ret, _, _ := procLineTo.Call( + uintptr(hdc), + uintptr(nXEnd), + uintptr(nYEnd)) + + return ret != 0 +} + +func MoveToEx(hdc HDC, x, y int, lpPoint *POINT) bool { + ret, _, _ := procMoveToEx.Call( + uintptr(hdc), + uintptr(x), + uintptr(y), + uintptr(unsafe.Pointer(lpPoint))) + + return ret != 0 +} + +func PlayEnhMetaFile(hdc HDC, hemf HENHMETAFILE, lpRect *RECT) bool { + ret, _, _ := procPlayEnhMetaFile.Call( + uintptr(hdc), + uintptr(hemf), + uintptr(unsafe.Pointer(lpRect))) + + return ret != 0 +} + +func Rectangle(hdc HDC, nLeftRect, nTopRect, nRightRect, nBottomRect int32) bool { + ret, _, _ := procRectangle.Call( + uintptr(hdc), + uintptr(nLeftRect), + uintptr(nTopRect), + uintptr(nRightRect), + uintptr(nBottomRect)) + + return ret != 0 +} + +func ResetDC(hdc HDC, lpInitData *DEVMODE) HDC { + ret, _, _ := procResetDC.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(lpInitData))) + + return HDC(ret) +} + +func SelectObject(hdc HDC, hgdiobj HGDIOBJ) HGDIOBJ { + ret, _, _ := procSelectObject.Call( + uintptr(hdc), + uintptr(hgdiobj)) + + if ret == 0 { + panic("SelectObject failed") + } + + return HGDIOBJ(ret) +} + +func SetBkMode(hdc HDC, iBkMode int) int { + ret, _, _ := procSetBkMode.Call( + uintptr(hdc), + uintptr(iBkMode)) + + if ret == 0 { + panic("SetBkMode failed") + } + + return int(ret) +} + +func SetBrushOrgEx(hdc HDC, nXOrg, nYOrg int, lppt *POINT) bool { + ret, _, _ := procSetBrushOrgEx.Call( + uintptr(hdc), + uintptr(nXOrg), + uintptr(nYOrg), + uintptr(unsafe.Pointer(lppt))) + + return ret != 0 +} + +func SetStretchBltMode(hdc HDC, iStretchMode int) int { + ret, _, _ := procSetStretchBltMode.Call( + uintptr(hdc), + uintptr(iStretchMode)) + + return int(ret) +} + +func SetTextColor(hdc HDC, crColor COLORREF) COLORREF { + ret, _, _ := procSetTextColor.Call( + uintptr(hdc), + uintptr(crColor)) + + if ret == CLR_INVALID { + panic("SetTextColor failed") + } + + return COLORREF(ret) +} + +func SetBkColor(hdc HDC, crColor COLORREF) COLORREF { + ret, _, _ := procSetBkColor.Call( + uintptr(hdc), + uintptr(crColor)) + + if ret == CLR_INVALID { + panic("SetBkColor failed") + } + + return COLORREF(ret) +} + +func StartDoc(hdc HDC, lpdi *DOCINFO) int { + ret, _, _ := procStartDoc.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(lpdi))) + + return int(ret) +} + +func StartPage(hdc HDC) int { + ret, _, _ := procStartPage.Call( + uintptr(hdc)) + + return int(ret) +} + +func StretchBlt(hdcDest HDC, nXOriginDest, nYOriginDest, nWidthDest, nHeightDest int, hdcSrc HDC, nXOriginSrc, nYOriginSrc, nWidthSrc, nHeightSrc int, dwRop uint) { + ret, _, _ := procStretchBlt.Call( + uintptr(hdcDest), + uintptr(nXOriginDest), + uintptr(nYOriginDest), + uintptr(nWidthDest), + uintptr(nHeightDest), + uintptr(hdcSrc), + uintptr(nXOriginSrc), + uintptr(nYOriginSrc), + uintptr(nWidthSrc), + uintptr(nHeightSrc), + uintptr(dwRop)) + + if ret == 0 { + panic("StretchBlt failed") + } +} + +func SetDIBitsToDevice(hdc HDC, xDest, yDest, dwWidth, dwHeight, xSrc, ySrc int, uStartScan, cScanLines uint, lpvBits []byte, lpbmi *BITMAPINFO, fuColorUse uint) int { + ret, _, _ := procSetDIBitsToDevice.Call( + uintptr(hdc), + uintptr(xDest), + uintptr(yDest), + uintptr(dwWidth), + uintptr(dwHeight), + uintptr(xSrc), + uintptr(ySrc), + uintptr(uStartScan), + uintptr(cScanLines), + uintptr(unsafe.Pointer(&lpvBits[0])), + uintptr(unsafe.Pointer(lpbmi)), + uintptr(fuColorUse)) + + return int(ret) +} + +func ChoosePixelFormat(hdc HDC, pfd *PIXELFORMATDESCRIPTOR) int { + ret, _, _ := procChoosePixelFormat.Call( + uintptr(hdc), + uintptr(unsafe.Pointer(pfd)), + ) + return int(ret) +} + +func DescribePixelFormat(hdc HDC, iPixelFormat int, nBytes uint, pfd *PIXELFORMATDESCRIPTOR) int { + ret, _, _ := procDescribePixelFormat.Call( + uintptr(hdc), + uintptr(iPixelFormat), + uintptr(nBytes), + uintptr(unsafe.Pointer(pfd)), + ) + return int(ret) +} + +func GetEnhMetaFilePixelFormat(hemf HENHMETAFILE, cbBuffer uint32, pfd *PIXELFORMATDESCRIPTOR) uint { + ret, _, _ := procGetEnhMetaFilePixelFormat.Call( + uintptr(hemf), + uintptr(cbBuffer), + uintptr(unsafe.Pointer(pfd)), + ) + return uint(ret) +} + +func GetPixelFormat(hdc HDC) int { + ret, _, _ := procGetPixelFormat.Call( + uintptr(hdc), + ) + return int(ret) +} + +func SetPixelFormat(hdc HDC, iPixelFormat int, pfd *PIXELFORMATDESCRIPTOR) bool { + ret, _, _ := procSetPixelFormat.Call( + uintptr(hdc), + uintptr(iPixelFormat), + uintptr(unsafe.Pointer(pfd)), + ) + return ret == TRUE +} + +func SwapBuffers(hdc HDC) bool { + ret, _, _ := procSwapBuffers.Call(uintptr(hdc)) + return ret == TRUE +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/gdiplus.go b/v2/internal/frontend/desktop/windows/winc/w32/gdiplus.go new file mode 100644 index 000000000..2591ed71b --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/gdiplus.go @@ -0,0 +1,177 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "errors" + "fmt" + "syscall" + "unsafe" +) + +const ( + Ok = 0 + GenericError = 1 + InvalidParameter = 2 + OutOfMemory = 3 + ObjectBusy = 4 + InsufficientBuffer = 5 + NotImplemented = 6 + Win32Error = 7 + WrongState = 8 + Aborted = 9 + FileNotFound = 10 + ValueOverflow = 11 + AccessDenied = 12 + UnknownImageFormat = 13 + FontFamilyNotFound = 14 + FontStyleNotFound = 15 + NotTrueTypeFont = 16 + UnsupportedGdiplusVersion = 17 + GdiplusNotInitialized = 18 + PropertyNotFound = 19 + PropertyNotSupported = 20 + ProfileNotFound = 21 +) + +func GetGpStatus(s int32) string { + switch s { + case Ok: + return "Ok" + case GenericError: + return "GenericError" + case InvalidParameter: + return "InvalidParameter" + case OutOfMemory: + return "OutOfMemory" + case ObjectBusy: + return "ObjectBusy" + case InsufficientBuffer: + return "InsufficientBuffer" + case NotImplemented: + return "NotImplemented" + case Win32Error: + return "Win32Error" + case WrongState: + return "WrongState" + case Aborted: + return "Aborted" + case FileNotFound: + return "FileNotFound" + case ValueOverflow: + return "ValueOverflow" + case AccessDenied: + return "AccessDenied" + case UnknownImageFormat: + return "UnknownImageFormat" + case FontFamilyNotFound: + return "FontFamilyNotFound" + case FontStyleNotFound: + return "FontStyleNotFound" + case NotTrueTypeFont: + return "NotTrueTypeFont" + case UnsupportedGdiplusVersion: + return "UnsupportedGdiplusVersion" + case GdiplusNotInitialized: + return "GdiplusNotInitialized" + case PropertyNotFound: + return "PropertyNotFound" + case PropertyNotSupported: + return "PropertyNotSupported" + case ProfileNotFound: + return "ProfileNotFound" + } + return "Unknown Status Value" +} + +var ( + token uintptr + + modgdiplus = syscall.NewLazyDLL("gdiplus.dll") + + procGdipCreateBitmapFromFile = modgdiplus.NewProc("GdipCreateBitmapFromFile") + procGdipCreateBitmapFromHBITMAP = modgdiplus.NewProc("GdipCreateBitmapFromHBITMAP") + procGdipCreateHBITMAPFromBitmap = modgdiplus.NewProc("GdipCreateHBITMAPFromBitmap") + procGdipCreateBitmapFromResource = modgdiplus.NewProc("GdipCreateBitmapFromResource") + procGdipCreateBitmapFromStream = modgdiplus.NewProc("GdipCreateBitmapFromStream") + procGdipDisposeImage = modgdiplus.NewProc("GdipDisposeImage") + procGdiplusShutdown = modgdiplus.NewProc("GdiplusShutdown") + procGdiplusStartup = modgdiplus.NewProc("GdiplusStartup") +) + +func GdipCreateBitmapFromFile(filename string) (*uintptr, error) { + var bitmap *uintptr + ret, _, _ := procGdipCreateBitmapFromFile.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(filename))), + uintptr(unsafe.Pointer(&bitmap))) + + if ret != Ok { + return nil, errors.New(fmt.Sprintf("GdipCreateBitmapFromFile failed with status '%s' for file '%s'", GetGpStatus(int32(ret)), filename)) + } + + return bitmap, nil +} + +func GdipCreateBitmapFromResource(instance HINSTANCE, resId *uint16) (*uintptr, error) { + var bitmap *uintptr + ret, _, _ := procGdipCreateBitmapFromResource.Call( + uintptr(instance), + uintptr(unsafe.Pointer(resId)), + uintptr(unsafe.Pointer(&bitmap))) + + if ret != Ok { + return nil, errors.New(fmt.Sprintf("GdiCreateBitmapFromResource failed with status '%s'", GetGpStatus(int32(ret)))) + } + + return bitmap, nil +} + +func GdipCreateBitmapFromStream(stream *IStream) (*uintptr, error) { + var bitmap *uintptr + ret, _, _ := procGdipCreateBitmapFromStream.Call( + uintptr(unsafe.Pointer(stream)), + uintptr(unsafe.Pointer(&bitmap))) + + if ret != Ok { + return nil, errors.New(fmt.Sprintf("GdipCreateBitmapFromStream failed with status '%s'", GetGpStatus(int32(ret)))) + } + + return bitmap, nil +} + +func GdipCreateHBITMAPFromBitmap(bitmap *uintptr, background uint32) (HBITMAP, error) { + var hbitmap HBITMAP + ret, _, _ := procGdipCreateHBITMAPFromBitmap.Call( + uintptr(unsafe.Pointer(bitmap)), + uintptr(unsafe.Pointer(&hbitmap)), + uintptr(background)) + + if ret != Ok { + return 0, errors.New(fmt.Sprintf("GdipCreateHBITMAPFromBitmap failed with status '%s'", GetGpStatus(int32(ret)))) + } + + return hbitmap, nil +} + +func GdipDisposeImage(image *uintptr) { + procGdipDisposeImage.Call(uintptr(unsafe.Pointer(image))) +} + +func GdiplusShutdown() { + procGdiplusShutdown.Call(token) +} + +func GdiplusStartup(input *GdiplusStartupInput, output *GdiplusStartupOutput) { + ret, _, _ := procGdiplusStartup.Call( + uintptr(unsafe.Pointer(&token)), + uintptr(unsafe.Pointer(input)), + uintptr(unsafe.Pointer(output))) + + if ret != Ok { + panic("GdiplusStartup failed with status " + GetGpStatus(int32(ret))) + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/idispatch.go b/v2/internal/frontend/desktop/windows/winc/w32/idispatch.go new file mode 100644 index 000000000..4f610f3ff --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/idispatch.go @@ -0,0 +1,45 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "unsafe" +) + +type pIDispatchVtbl struct { + pQueryInterface uintptr + pAddRef uintptr + pRelease uintptr + pGetTypeInfoCount uintptr + pGetTypeInfo uintptr + pGetIDsOfNames uintptr + pInvoke uintptr +} + +type IDispatch struct { + lpVtbl *pIDispatchVtbl +} + +func (this *IDispatch) QueryInterface(id *GUID) *IDispatch { + return ComQueryInterface((*IUnknown)(unsafe.Pointer(this)), id) +} + +func (this *IDispatch) AddRef() int32 { + return ComAddRef((*IUnknown)(unsafe.Pointer(this))) +} + +func (this *IDispatch) Release() int32 { + return ComRelease((*IUnknown)(unsafe.Pointer(this))) +} + +func (this *IDispatch) GetIDsOfName(names []string) []int32 { + return ComGetIDsOfName(this, names) +} + +func (this *IDispatch) Invoke(dispid int32, dispatch int16, params ...interface{}) *VARIANT { + return ComInvoke(this, dispid, dispatch, params...) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/istream.go b/v2/internal/frontend/desktop/windows/winc/w32/istream.go new file mode 100644 index 000000000..a47fbbce1 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/istream.go @@ -0,0 +1,33 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "unsafe" +) + +type pIStreamVtbl struct { + pQueryInterface uintptr + pAddRef uintptr + pRelease uintptr +} + +type IStream struct { + lpVtbl *pIStreamVtbl +} + +func (this *IStream) QueryInterface(id *GUID) *IDispatch { + return ComQueryInterface((*IUnknown)(unsafe.Pointer(this)), id) +} + +func (this *IStream) AddRef() int32 { + return ComAddRef((*IUnknown)(unsafe.Pointer(this))) +} + +func (this *IStream) Release() int32 { + return ComRelease((*IUnknown)(unsafe.Pointer(this))) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/iunknown.go b/v2/internal/frontend/desktop/windows/winc/w32/iunknown.go new file mode 100644 index 000000000..8ddc605cc --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/iunknown.go @@ -0,0 +1,29 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +type pIUnknownVtbl struct { + pQueryInterface uintptr + pAddRef uintptr + pRelease uintptr +} + +type IUnknown struct { + lpVtbl *pIUnknownVtbl +} + +func (this *IUnknown) QueryInterface(id *GUID) *IDispatch { + return ComQueryInterface(this, id) +} + +func (this *IUnknown) AddRef() int32 { + return ComAddRef(this) +} + +func (this *IUnknown) Release() int32 { + return ComRelease(this) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/kernel32.go b/v2/internal/frontend/desktop/windows/winc/w32/kernel32.go new file mode 100644 index 000000000..063a1b0ea --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/kernel32.go @@ -0,0 +1,332 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + + procGetModuleHandle = modkernel32.NewProc("GetModuleHandleW") + procMulDiv = modkernel32.NewProc("MulDiv") + procGetConsoleWindow = modkernel32.NewProc("GetConsoleWindow") + procGetCurrentThread = modkernel32.NewProc("GetCurrentThread") + procGetCurrentThreadId = modkernel32.NewProc("GetCurrentThreadId") + procGetLogicalDrives = modkernel32.NewProc("GetLogicalDrives") + procGetLogicalDriveStrings = modkernel32.NewProc("GetLogicalDriveStringsW") + procGetUserDefaultLCID = modkernel32.NewProc("GetUserDefaultLCID") + procLstrlen = modkernel32.NewProc("lstrlenW") + procLstrcpy = modkernel32.NewProc("lstrcpyW") + procGlobalAlloc = modkernel32.NewProc("GlobalAlloc") + procGlobalFree = modkernel32.NewProc("GlobalFree") + procGlobalLock = modkernel32.NewProc("GlobalLock") + procGlobalUnlock = modkernel32.NewProc("GlobalUnlock") + procMoveMemory = modkernel32.NewProc("RtlMoveMemory") + procFindResource = modkernel32.NewProc("FindResourceW") + procSizeofResource = modkernel32.NewProc("SizeofResource") + procLockResource = modkernel32.NewProc("LockResource") + procLoadResource = modkernel32.NewProc("LoadResource") + procGetLastError = modkernel32.NewProc("GetLastError") + procOpenProcess = modkernel32.NewProc("OpenProcess") + procTerminateProcess = modkernel32.NewProc("TerminateProcess") + procCloseHandle = modkernel32.NewProc("CloseHandle") + procCreateToolhelp32Snapshot = modkernel32.NewProc("CreateToolhelp32Snapshot") + procModule32First = modkernel32.NewProc("Module32FirstW") + procModule32Next = modkernel32.NewProc("Module32NextW") + procGetSystemTimes = modkernel32.NewProc("GetSystemTimes") + procGetConsoleScreenBufferInfo = modkernel32.NewProc("GetConsoleScreenBufferInfo") + procSetConsoleTextAttribute = modkernel32.NewProc("SetConsoleTextAttribute") + procGetDiskFreeSpaceEx = modkernel32.NewProc("GetDiskFreeSpaceExW") + procGetProcessTimes = modkernel32.NewProc("GetProcessTimes") + procSetSystemTime = modkernel32.NewProc("SetSystemTime") + procGetSystemTime = modkernel32.NewProc("GetSystemTime") +) + +func GetModuleHandle(modulename string) HINSTANCE { + var mn uintptr + if modulename == "" { + mn = 0 + } else { + mn = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(modulename))) + } + ret, _, _ := procGetModuleHandle.Call(mn) + return HINSTANCE(ret) +} + +func MulDiv(number, numerator, denominator int) int { + ret, _, _ := procMulDiv.Call( + uintptr(number), + uintptr(numerator), + uintptr(denominator)) + + return int(ret) +} + +func GetConsoleWindow() HWND { + ret, _, _ := procGetConsoleWindow.Call() + + return HWND(ret) +} + +func GetCurrentThread() HANDLE { + ret, _, _ := procGetCurrentThread.Call() + + return HANDLE(ret) +} + +func GetCurrentThreadId() HANDLE { + ret, _, _ := procGetCurrentThreadId.Call() + + return HANDLE(ret) +} + +func GetLogicalDrives() uint32 { + ret, _, _ := procGetLogicalDrives.Call() + + return uint32(ret) +} + +func GetUserDefaultLCID() uint32 { + ret, _, _ := procGetUserDefaultLCID.Call() + + return uint32(ret) +} + +func Lstrlen(lpString *uint16) int { + ret, _, _ := procLstrlen.Call(uintptr(unsafe.Pointer(lpString))) + + return int(ret) +} + +func Lstrcpy(buf []uint16, lpString *uint16) { + procLstrcpy.Call( + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(lpString))) +} + +func GlobalAlloc(uFlags uint, dwBytes uint32) HGLOBAL { + ret, _, _ := procGlobalAlloc.Call( + uintptr(uFlags), + uintptr(dwBytes)) + + if ret == 0 { + panic("GlobalAlloc failed") + } + + return HGLOBAL(ret) +} + +func GlobalFree(hMem HGLOBAL) { + ret, _, _ := procGlobalFree.Call(uintptr(hMem)) + + if ret != 0 { + panic("GlobalFree failed") + } +} + +func GlobalLock(hMem HGLOBAL) unsafe.Pointer { + ret, _, _ := procGlobalLock.Call(uintptr(hMem)) + + if ret == 0 { + panic("GlobalLock failed") + } + + return unsafe.Pointer(ret) +} + +func GlobalUnlock(hMem HGLOBAL) bool { + ret, _, _ := procGlobalUnlock.Call(uintptr(hMem)) + + return ret != 0 +} + +func MoveMemory(destination, source unsafe.Pointer, length uint32) { + procMoveMemory.Call( + uintptr(unsafe.Pointer(destination)), + uintptr(source), + uintptr(length)) +} + +func FindResource(hModule HMODULE, lpName, lpType *uint16) (HRSRC, error) { + ret, _, _ := procFindResource.Call( + uintptr(hModule), + uintptr(unsafe.Pointer(lpName)), + uintptr(unsafe.Pointer(lpType))) + + if ret == 0 { + return 0, syscall.GetLastError() + } + + return HRSRC(ret), nil +} + +func SizeofResource(hModule HMODULE, hResInfo HRSRC) uint32 { + ret, _, _ := procSizeofResource.Call( + uintptr(hModule), + uintptr(hResInfo)) + + if ret == 0 { + panic("SizeofResource failed") + } + + return uint32(ret) +} + +func LockResource(hResData HGLOBAL) unsafe.Pointer { + ret, _, _ := procLockResource.Call(uintptr(hResData)) + + if ret == 0 { + panic("LockResource failed") + } + + return unsafe.Pointer(ret) +} + +func LoadResource(hModule HMODULE, hResInfo HRSRC) HGLOBAL { + ret, _, _ := procLoadResource.Call( + uintptr(hModule), + uintptr(hResInfo)) + + if ret == 0 { + panic("LoadResource failed") + } + + return HGLOBAL(ret) +} + +func GetLastError() uint32 { + ret, _, _ := procGetLastError.Call() + return uint32(ret) +} + +func OpenProcess(desiredAccess uint32, inheritHandle bool, processId uint32) HANDLE { + inherit := 0 + if inheritHandle { + inherit = 1 + } + + ret, _, _ := procOpenProcess.Call( + uintptr(desiredAccess), + uintptr(inherit), + uintptr(processId)) + return HANDLE(ret) +} + +func TerminateProcess(hProcess HANDLE, uExitCode uint) bool { + ret, _, _ := procTerminateProcess.Call( + uintptr(hProcess), + uintptr(uExitCode)) + return ret != 0 +} + +func CloseHandle(object HANDLE) bool { + ret, _, _ := procCloseHandle.Call( + uintptr(object)) + return ret != 0 +} + +func CreateToolhelp32Snapshot(flags, processId uint32) HANDLE { + ret, _, _ := procCreateToolhelp32Snapshot.Call( + uintptr(flags), + uintptr(processId)) + + if ret <= 0 { + return HANDLE(0) + } + + return HANDLE(ret) +} + +func Module32First(snapshot HANDLE, me *MODULEENTRY32) bool { + ret, _, _ := procModule32First.Call( + uintptr(snapshot), + uintptr(unsafe.Pointer(me))) + + return ret != 0 +} + +func Module32Next(snapshot HANDLE, me *MODULEENTRY32) bool { + ret, _, _ := procModule32Next.Call( + uintptr(snapshot), + uintptr(unsafe.Pointer(me))) + + return ret != 0 +} + +func GetSystemTimes(lpIdleTime, lpKernelTime, lpUserTime *FILETIME) bool { + ret, _, _ := procGetSystemTimes.Call( + uintptr(unsafe.Pointer(lpIdleTime)), + uintptr(unsafe.Pointer(lpKernelTime)), + uintptr(unsafe.Pointer(lpUserTime))) + + return ret != 0 +} + +func GetProcessTimes(hProcess HANDLE, lpCreationTime, lpExitTime, lpKernelTime, lpUserTime *FILETIME) bool { + ret, _, _ := procGetProcessTimes.Call( + uintptr(hProcess), + uintptr(unsafe.Pointer(lpCreationTime)), + uintptr(unsafe.Pointer(lpExitTime)), + uintptr(unsafe.Pointer(lpKernelTime)), + uintptr(unsafe.Pointer(lpUserTime))) + + return ret != 0 +} + +func GetConsoleScreenBufferInfo(hConsoleOutput HANDLE) *CONSOLE_SCREEN_BUFFER_INFO { + var csbi CONSOLE_SCREEN_BUFFER_INFO + ret, _, _ := procGetConsoleScreenBufferInfo.Call( + uintptr(hConsoleOutput), + uintptr(unsafe.Pointer(&csbi))) + if ret == 0 { + return nil + } + return &csbi +} + +func SetConsoleTextAttribute(hConsoleOutput HANDLE, wAttributes uint16) bool { + ret, _, _ := procSetConsoleTextAttribute.Call( + uintptr(hConsoleOutput), + uintptr(wAttributes)) + return ret != 0 +} + +func GetDiskFreeSpaceEx(dirName string) (r bool, + freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64) { + ret, _, _ := procGetDiskFreeSpaceEx.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(dirName))), + uintptr(unsafe.Pointer(&freeBytesAvailable)), + uintptr(unsafe.Pointer(&totalNumberOfBytes)), + uintptr(unsafe.Pointer(&totalNumberOfFreeBytes))) + return ret != 0, + freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes +} + +func GetSystemTime() *SYSTEMTIME { + var time SYSTEMTIME + procGetSystemTime.Call( + uintptr(unsafe.Pointer(&time))) + return &time +} + +func SetSystemTime(time *SYSTEMTIME) bool { + ret, _, _ := procSetSystemTime.Call( + uintptr(unsafe.Pointer(time))) + return ret != 0 +} + +func GetLogicalDriveStrings(nBufferLength uint32, lpBuffer *uint16) uint32 { + ret, _, _ := procGetLogicalDriveStrings.Call( + uintptr(nBufferLength), + uintptr(unsafe.Pointer(lpBuffer)), + 0) + + return uint32(ret) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/ole32.go b/v2/internal/frontend/desktop/windows/winc/w32/ole32.go new file mode 100644 index 000000000..004099316 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/ole32.go @@ -0,0 +1,65 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modole32 = syscall.NewLazyDLL("ole32.dll") + + procCoInitializeEx = modole32.NewProc("CoInitializeEx") + procCoInitialize = modole32.NewProc("CoInitialize") + procCoUninitialize = modole32.NewProc("CoUninitialize") + procCreateStreamOnHGlobal = modole32.NewProc("CreateStreamOnHGlobal") +) + +func CoInitializeEx(coInit uintptr) HRESULT { + ret, _, _ := procCoInitializeEx.Call( + 0, + coInit) + + switch uint32(ret) { + case E_INVALIDARG: + panic("CoInitializeEx failed with E_INVALIDARG") + case E_OUTOFMEMORY: + panic("CoInitializeEx failed with E_OUTOFMEMORY") + case E_UNEXPECTED: + panic("CoInitializeEx failed with E_UNEXPECTED") + } + + return HRESULT(ret) +} + +func CoInitialize() { + procCoInitialize.Call(0) +} + +func CoUninitialize() { + procCoUninitialize.Call() +} + +func CreateStreamOnHGlobal(hGlobal HGLOBAL, fDeleteOnRelease bool) *IStream { + stream := new(IStream) + ret, _, _ := procCreateStreamOnHGlobal.Call( + uintptr(hGlobal), + uintptr(BoolToBOOL(fDeleteOnRelease)), + uintptr(unsafe.Pointer(&stream))) + + switch uint32(ret) { + case E_INVALIDARG: + panic("CreateStreamOnHGlobal failed with E_INVALIDARG") + case E_OUTOFMEMORY: + panic("CreateStreamOnHGlobal failed with E_OUTOFMEMORY") + case E_UNEXPECTED: + panic("CreateStreamOnHGlobal failed with E_UNEXPECTED") + } + + return stream +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/oleaut32.go b/v2/internal/frontend/desktop/windows/winc/w32/oleaut32.go new file mode 100644 index 000000000..0bb8ef7da --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/oleaut32.go @@ -0,0 +1,50 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modoleaut32 = syscall.NewLazyDLL("oleaut32") + + procVariantInit = modoleaut32.NewProc("VariantInit") + procSysAllocString = modoleaut32.NewProc("SysAllocString") + procSysFreeString = modoleaut32.NewProc("SysFreeString") + procSysStringLen = modoleaut32.NewProc("SysStringLen") + procCreateDispTypeInfo = modoleaut32.NewProc("CreateDispTypeInfo") + procCreateStdDispatch = modoleaut32.NewProc("CreateStdDispatch") +) + +func VariantInit(v *VARIANT) { + hr, _, _ := procVariantInit.Call(uintptr(unsafe.Pointer(v))) + if hr != 0 { + panic("Invoke VariantInit error.") + } + return +} + +func SysAllocString(v string) (ss *int16) { + pss, _, _ := procSysAllocString.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(v)))) + ss = (*int16)(unsafe.Pointer(pss)) + return +} + +func SysFreeString(v *int16) { + hr, _, _ := procSysFreeString.Call(uintptr(unsafe.Pointer(v))) + if hr != 0 { + panic("Invoke SysFreeString error.") + } + return +} + +func SysStringLen(v *int16) uint { + l, _, _ := procSysStringLen.Call(uintptr(unsafe.Pointer(v))) + return uint(l) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/shcore.go b/v2/internal/frontend/desktop/windows/winc/w32/shcore.go new file mode 100644 index 000000000..72e9aab3d --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/shcore.go @@ -0,0 +1,29 @@ +//go:build windows + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modshcore = syscall.NewLazyDLL("shcore.dll") + + procGetDpiForMonitor = modshcore.NewProc("GetDpiForMonitor") +) + +func HasGetDPIForMonitorFunc() bool { + err := procGetDpiForMonitor.Find() + return err == nil +} + +func GetDPIForMonitor(hmonitor HMONITOR, dpiType MONITOR_DPI_TYPE, dpiX *UINT, dpiY *UINT) uintptr { + ret, _, _ := procGetDpiForMonitor.Call( + hmonitor, + uintptr(dpiType), + uintptr(unsafe.Pointer(dpiX)), + uintptr(unsafe.Pointer(dpiY))) + + return ret +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/shell32.go b/v2/internal/frontend/desktop/windows/winc/w32/shell32.go new file mode 100644 index 000000000..458fbc645 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/shell32.go @@ -0,0 +1,235 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ +package w32 + +import ( + "errors" + "fmt" + "syscall" + "unsafe" +) + +type CSIDL uint32 + +const ( + CSIDL_DESKTOP = 0x00 + CSIDL_INTERNET = 0x01 + CSIDL_PROGRAMS = 0x02 + CSIDL_CONTROLS = 0x03 + CSIDL_PRINTERS = 0x04 + CSIDL_PERSONAL = 0x05 + CSIDL_FAVORITES = 0x06 + CSIDL_STARTUP = 0x07 + CSIDL_RECENT = 0x08 + CSIDL_SENDTO = 0x09 + CSIDL_BITBUCKET = 0x0A + CSIDL_STARTMENU = 0x0B + CSIDL_MYDOCUMENTS = 0x0C + CSIDL_MYMUSIC = 0x0D + CSIDL_MYVIDEO = 0x0E + CSIDL_DESKTOPDIRECTORY = 0x10 + CSIDL_DRIVES = 0x11 + CSIDL_NETWORK = 0x12 + CSIDL_NETHOOD = 0x13 + CSIDL_FONTS = 0x14 + CSIDL_TEMPLATES = 0x15 + CSIDL_COMMON_STARTMENU = 0x16 + CSIDL_COMMON_PROGRAMS = 0x17 + CSIDL_COMMON_STARTUP = 0x18 + CSIDL_COMMON_DESKTOPDIRECTORY = 0x19 + CSIDL_APPDATA = 0x1A + CSIDL_PRINTHOOD = 0x1B + CSIDL_LOCAL_APPDATA = 0x1C + CSIDL_ALTSTARTUP = 0x1D + CSIDL_COMMON_ALTSTARTUP = 0x1E + CSIDL_COMMON_FAVORITES = 0x1F + CSIDL_INTERNET_CACHE = 0x20 + CSIDL_COOKIES = 0x21 + CSIDL_HISTORY = 0x22 + CSIDL_COMMON_APPDATA = 0x23 + CSIDL_WINDOWS = 0x24 + CSIDL_SYSTEM = 0x25 + CSIDL_PROGRAM_FILES = 0x26 + CSIDL_MYPICTURES = 0x27 + CSIDL_PROFILE = 0x28 + CSIDL_SYSTEMX86 = 0x29 + CSIDL_PROGRAM_FILESX86 = 0x2A + CSIDL_PROGRAM_FILES_COMMON = 0x2B + CSIDL_PROGRAM_FILES_COMMONX86 = 0x2C + CSIDL_COMMON_TEMPLATES = 0x2D + CSIDL_COMMON_DOCUMENTS = 0x2E + CSIDL_COMMON_ADMINTOOLS = 0x2F + CSIDL_ADMINTOOLS = 0x30 + CSIDL_CONNECTIONS = 0x31 + CSIDL_COMMON_MUSIC = 0x35 + CSIDL_COMMON_PICTURES = 0x36 + CSIDL_COMMON_VIDEO = 0x37 + CSIDL_RESOURCES = 0x38 + CSIDL_RESOURCES_LOCALIZED = 0x39 + CSIDL_COMMON_OEM_LINKS = 0x3A + CSIDL_CDBURN_AREA = 0x3B + CSIDL_COMPUTERSNEARME = 0x3D + CSIDL_FLAG_CREATE = 0x8000 + CSIDL_FLAG_DONT_VERIFY = 0x4000 + CSIDL_FLAG_NO_ALIAS = 0x1000 + CSIDL_FLAG_PER_USER_INIT = 0x8000 + CSIDL_FLAG_MASK = 0xFF00 +) + +var ( + modshell32 = syscall.NewLazyDLL("shell32.dll") + + procSHBrowseForFolder = modshell32.NewProc("SHBrowseForFolderW") + procSHGetPathFromIDList = modshell32.NewProc("SHGetPathFromIDListW") + procDragAcceptFiles = modshell32.NewProc("DragAcceptFiles") + procDragQueryFile = modshell32.NewProc("DragQueryFileW") + procDragQueryPoint = modshell32.NewProc("DragQueryPoint") + procDragFinish = modshell32.NewProc("DragFinish") + procShellExecute = modshell32.NewProc("ShellExecuteW") + procExtractIcon = modshell32.NewProc("ExtractIconW") + procGetSpecialFolderPath = modshell32.NewProc("SHGetSpecialFolderPathW") +) + +func SHBrowseForFolder(bi *BROWSEINFO) uintptr { + ret, _, _ := procSHBrowseForFolder.Call(uintptr(unsafe.Pointer(bi))) + + return ret +} + +func SHGetPathFromIDList(idl uintptr) string { + buf := make([]uint16, 1024) + procSHGetPathFromIDList.Call( + idl, + uintptr(unsafe.Pointer(&buf[0]))) + + return syscall.UTF16ToString(buf) +} + +func DragAcceptFiles(hwnd HWND, accept bool) { + procDragAcceptFiles.Call( + uintptr(hwnd), + uintptr(BoolToBOOL(accept))) +} + +func DragQueryFile(hDrop HDROP, iFile uint) (fileName string, fileCount uint) { + ret, _, _ := procDragQueryFile.Call( + uintptr(hDrop), + uintptr(iFile), + 0, + 0) + + fileCount = uint(ret) + + if iFile != 0xFFFFFFFF { + buf := make([]uint16, fileCount+1) + + ret, _, _ := procDragQueryFile.Call( + uintptr(hDrop), + uintptr(iFile), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(fileCount+1)) + + if ret == 0 { + panic("Invoke DragQueryFile error.") + } + + fileName = syscall.UTF16ToString(buf) + } + + return +} + +func DragQueryPoint(hDrop HDROP) (x, y int, isClientArea bool) { + var pt POINT + ret, _, _ := procDragQueryPoint.Call( + uintptr(hDrop), + uintptr(unsafe.Pointer(&pt))) + + return int(pt.X), int(pt.Y), (ret == 1) +} + +func DragFinish(hDrop HDROP) { + procDragFinish.Call(uintptr(hDrop)) +} + +func ShellExecute(hwnd HWND, lpOperation, lpFile, lpParameters, lpDirectory string, nShowCmd int) error { + var op, param, directory uintptr + if len(lpOperation) != 0 { + op = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpOperation))) + } + if len(lpParameters) != 0 { + param = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpParameters))) + } + if len(lpDirectory) != 0 { + directory = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpDirectory))) + } + + ret, _, _ := procShellExecute.Call( + uintptr(hwnd), + op, + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpFile))), + param, + directory, + uintptr(nShowCmd)) + + errorMsg := "" + if ret != 0 && ret <= 32 { + switch int(ret) { + case ERROR_FILE_NOT_FOUND: + errorMsg = "The specified file was not found." + case ERROR_PATH_NOT_FOUND: + errorMsg = "The specified path was not found." + case ERROR_BAD_FORMAT: + errorMsg = "The .exe file is invalid (non-Win32 .exe or error in .exe image)." + case SE_ERR_ACCESSDENIED: + errorMsg = "The operating system denied access to the specified file." + case SE_ERR_ASSOCINCOMPLETE: + errorMsg = "The file name association is incomplete or invalid." + case SE_ERR_DDEBUSY: + errorMsg = "The DDE transaction could not be completed because other DDE transactions were being processed." + case SE_ERR_DDEFAIL: + errorMsg = "The DDE transaction failed." + case SE_ERR_DDETIMEOUT: + errorMsg = "The DDE transaction could not be completed because the request timed out." + case SE_ERR_DLLNOTFOUND: + errorMsg = "The specified DLL was not found." + case SE_ERR_NOASSOC: + errorMsg = "There is no application associated with the given file name extension. This error will also be returned if you attempt to print a file that is not printable." + case SE_ERR_OOM: + errorMsg = "There was not enough memory to complete the operation." + case SE_ERR_SHARE: + errorMsg = "A sharing violation occurred." + default: + errorMsg = fmt.Sprintf("Unknown error occurred with error code %v", ret) + } + } else { + return nil + } + + return errors.New(errorMsg) +} + +func ExtractIcon(lpszExeFileName string, nIconIndex int) HICON { + ret, _, _ := procExtractIcon.Call( + 0, + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpszExeFileName))), + uintptr(nIconIndex)) + + return HICON(ret) +} + +func SHGetSpecialFolderPath(hwndOwner HWND, lpszPath *uint16, csidl CSIDL, fCreate bool) bool { + ret, _, _ := procGetSpecialFolderPath.Call( + uintptr(hwndOwner), + uintptr(unsafe.Pointer(lpszPath)), + uintptr(csidl), + uintptr(BoolToBOOL(fCreate)), + 0, + 0) + + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/shlwapi.go b/v2/internal/frontend/desktop/windows/winc/w32/shlwapi.go new file mode 100644 index 000000000..89d17ce6f --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/shlwapi.go @@ -0,0 +1,26 @@ +//go:build windows + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + modshlwapi = syscall.NewLazyDLL("shlwapi.dll") + + procSHCreateMemStream = modshlwapi.NewProc("SHCreateMemStream") +) + +func SHCreateMemStream(data []byte) (uintptr, error) { + ret, _, err := procSHCreateMemStream.Call( + uintptr(unsafe.Pointer(&data[0])), + uintptr(len(data)), + ) + if ret == 0 { + return 0, err + } + + return ret, nil +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/toolbar.go b/v2/internal/frontend/desktop/windows/winc/w32/toolbar.go new file mode 100644 index 000000000..ac9261fc4 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/toolbar.go @@ -0,0 +1,216 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +// ToolBar messages +const ( + TB_ENABLEBUTTON = WM_USER + 1 + TB_CHECKBUTTON = WM_USER + 2 + TB_PRESSBUTTON = WM_USER + 3 + TB_HIDEBUTTON = WM_USER + 4 + TB_INDETERMINATE = WM_USER + 5 + TB_MARKBUTTON = WM_USER + 6 + TB_ISBUTTONENABLED = WM_USER + 9 + TB_ISBUTTONCHECKED = WM_USER + 10 + TB_ISBUTTONPRESSED = WM_USER + 11 + TB_ISBUTTONHIDDEN = WM_USER + 12 + TB_ISBUTTONINDETERMINATE = WM_USER + 13 + TB_ISBUTTONHIGHLIGHTED = WM_USER + 14 + TB_SETSTATE = WM_USER + 17 + TB_GETSTATE = WM_USER + 18 + TB_ADDBITMAP = WM_USER + 19 + TB_DELETEBUTTON = WM_USER + 22 + TB_GETBUTTON = WM_USER + 23 + TB_BUTTONCOUNT = WM_USER + 24 + TB_COMMANDTOINDEX = WM_USER + 25 + TB_SAVERESTORE = WM_USER + 76 + TB_CUSTOMIZE = WM_USER + 27 + TB_ADDSTRING = WM_USER + 77 + TB_GETITEMRECT = WM_USER + 29 + TB_BUTTONSTRUCTSIZE = WM_USER + 30 + TB_SETBUTTONSIZE = WM_USER + 31 + TB_SETBITMAPSIZE = WM_USER + 32 + TB_AUTOSIZE = WM_USER + 33 + TB_GETTOOLTIPS = WM_USER + 35 + TB_SETTOOLTIPS = WM_USER + 36 + TB_SETPARENT = WM_USER + 37 + TB_SETROWS = WM_USER + 39 + TB_GETROWS = WM_USER + 40 + TB_GETBITMAPFLAGS = WM_USER + 41 + TB_SETCMDID = WM_USER + 42 + TB_CHANGEBITMAP = WM_USER + 43 + TB_GETBITMAP = WM_USER + 44 + TB_GETBUTTONTEXT = WM_USER + 75 + TB_REPLACEBITMAP = WM_USER + 46 + TB_GETBUTTONSIZE = WM_USER + 58 + TB_SETBUTTONWIDTH = WM_USER + 59 + TB_SETINDENT = WM_USER + 47 + TB_SETIMAGELIST = WM_USER + 48 + TB_GETIMAGELIST = WM_USER + 49 + TB_LOADIMAGES = WM_USER + 50 + TB_GETRECT = WM_USER + 51 + TB_SETHOTIMAGELIST = WM_USER + 52 + TB_GETHOTIMAGELIST = WM_USER + 53 + TB_SETDISABLEDIMAGELIST = WM_USER + 54 + TB_GETDISABLEDIMAGELIST = WM_USER + 55 + TB_SETSTYLE = WM_USER + 56 + TB_GETSTYLE = WM_USER + 57 + TB_SETMAXTEXTROWS = WM_USER + 60 + TB_GETTEXTROWS = WM_USER + 61 + TB_GETOBJECT = WM_USER + 62 + TB_GETBUTTONINFO = WM_USER + 63 + TB_SETBUTTONINFO = WM_USER + 64 + TB_INSERTBUTTON = WM_USER + 67 + TB_ADDBUTTONS = WM_USER + 68 + TB_HITTEST = WM_USER + 69 + TB_SETDRAWTEXTFLAGS = WM_USER + 70 + TB_GETHOTITEM = WM_USER + 71 + TB_SETHOTITEM = WM_USER + 72 + TB_SETANCHORHIGHLIGHT = WM_USER + 73 + TB_GETANCHORHIGHLIGHT = WM_USER + 74 + TB_GETINSERTMARK = WM_USER + 79 + TB_SETINSERTMARK = WM_USER + 80 + TB_INSERTMARKHITTEST = WM_USER + 81 + TB_MOVEBUTTON = WM_USER + 82 + TB_GETMAXSIZE = WM_USER + 83 + TB_SETEXTENDEDSTYLE = WM_USER + 84 + TB_GETEXTENDEDSTYLE = WM_USER + 85 + TB_GETPADDING = WM_USER + 86 + TB_SETPADDING = WM_USER + 87 + TB_SETINSERTMARKCOLOR = WM_USER + 88 + TB_GETINSERTMARKCOLOR = WM_USER + 89 + TB_MAPACCELERATOR = WM_USER + 90 + TB_GETSTRING = WM_USER + 91 + TB_SETCOLORSCHEME = CCM_SETCOLORSCHEME + TB_GETCOLORSCHEME = CCM_GETCOLORSCHEME + TB_SETUNICODEFORMAT = CCM_SETUNICODEFORMAT + TB_GETUNICODEFORMAT = CCM_GETUNICODEFORMAT +) + +// ToolBar notifications +const ( + TBN_FIRST = -700 + TBN_DROPDOWN = TBN_FIRST - 10 +) + +// TBN_DROPDOWN return codes +const ( + TBDDRET_DEFAULT = 0 + TBDDRET_NODEFAULT = 1 + TBDDRET_TREATPRESSED = 2 +) + +// ToolBar state constants +const ( + TBSTATE_CHECKED = 1 + TBSTATE_PRESSED = 2 + TBSTATE_ENABLED = 4 + TBSTATE_HIDDEN = 8 + TBSTATE_INDETERMINATE = 16 + TBSTATE_WRAP = 32 + TBSTATE_ELLIPSES = 0x40 + TBSTATE_MARKED = 0x0080 +) + +// ToolBar style constants +const ( + TBSTYLE_BUTTON = 0 + TBSTYLE_SEP = 1 + TBSTYLE_CHECK = 2 + TBSTYLE_GROUP = 4 + TBSTYLE_CHECKGROUP = TBSTYLE_GROUP | TBSTYLE_CHECK + TBSTYLE_DROPDOWN = 8 + TBSTYLE_AUTOSIZE = 16 + TBSTYLE_NOPREFIX = 32 + TBSTYLE_TOOLTIPS = 256 + TBSTYLE_WRAPABLE = 512 + TBSTYLE_ALTDRAG = 1024 + TBSTYLE_FLAT = 2048 + TBSTYLE_LIST = 4096 + TBSTYLE_CUSTOMERASE = 8192 + TBSTYLE_REGISTERDROP = 0x4000 + TBSTYLE_TRANSPARENT = 0x8000 +) + +// ToolBar extended style constants +const ( + TBSTYLE_EX_DRAWDDARROWS = 0x00000001 + TBSTYLE_EX_MIXEDBUTTONS = 8 + TBSTYLE_EX_HIDECLIPPEDBUTTONS = 16 + TBSTYLE_EX_DOUBLEBUFFER = 0x80 +) + +// ToolBar button style constants +const ( + BTNS_BUTTON = TBSTYLE_BUTTON + BTNS_SEP = TBSTYLE_SEP + BTNS_CHECK = TBSTYLE_CHECK + BTNS_GROUP = TBSTYLE_GROUP + BTNS_CHECKGROUP = TBSTYLE_CHECKGROUP + BTNS_DROPDOWN = TBSTYLE_DROPDOWN + BTNS_AUTOSIZE = TBSTYLE_AUTOSIZE + BTNS_NOPREFIX = TBSTYLE_NOPREFIX + BTNS_WHOLEDROPDOWN = 0x0080 + BTNS_SHOWTEXT = 0x0040 +) + +// TBBUTTONINFO mask flags +const ( + TBIF_IMAGE = 0x00000001 + TBIF_TEXT = 0x00000002 + TBIF_STATE = 0x00000004 + TBIF_STYLE = 0x00000008 + TBIF_LPARAM = 0x00000010 + TBIF_COMMAND = 0x00000020 + TBIF_SIZE = 0x00000040 + TBIF_BYINDEX = 0x80000000 +) + +type NMMOUSE struct { + Hdr NMHDR + DwItemSpec uintptr + DwItemData uintptr + Pt POINT + DwHitInfo uintptr +} + +type NMTOOLBAR struct { + Hdr NMHDR + IItem int32 + TbButton TBBUTTON + CchText int32 + PszText *uint16 + RcButton RECT +} + +type TBBUTTON struct { + IBitmap int32 + IdCommand int32 + FsState byte + FsStyle byte + //#ifdef _WIN64 + // BYTE bReserved[6] // padding for alignment + //#elif defined(_WIN32) + BReserved [2]byte // padding for alignment + //#endif + DwData uintptr + IString uintptr +} + +type TBBUTTONINFO struct { + CbSize uint32 + DwMask uint32 + IdCommand int32 + IImage int32 + FsState byte + FsStyle byte + Cx uint16 + LParam uintptr + PszText uintptr + CchText int32 +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/typedef.go b/v2/internal/frontend/desktop/windows/winc/w32/typedef.go new file mode 100644 index 000000000..13735204c --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/typedef.go @@ -0,0 +1,1081 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +import ( + "fmt" + "unsafe" +) + +// From MSDN: Windows Data Types +// http://msdn.microsoft.com/en-us/library/s3f49ktz.aspx +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx +// ATOM WORD +// BOOL int32 +// BOOLEAN byte +// BYTE byte +// CCHAR int8 +// CHAR int8 +// COLORREF DWORD +// DWORD uint32 +// DWORDLONG ULONGLONG +// DWORD_PTR ULONG_PTR +// DWORD32 uint32 +// DWORD64 uint64 +// FLOAT float32 +// HACCEL HANDLE +// HALF_PTR struct{} // ??? +// HANDLE PVOID +// HBITMAP HANDLE +// HBRUSH HANDLE +// HCOLORSPACE HANDLE +// HCONV HANDLE +// HCONVLIST HANDLE +// HCURSOR HANDLE +// HDC HANDLE +// HDDEDATA HANDLE +// HDESK HANDLE +// HDROP HANDLE +// HDWP HANDLE +// HENHMETAFILE HANDLE +// HFILE HANDLE +// HFONT HANDLE +// HGDIOBJ HANDLE +// HGLOBAL HANDLE +// HHOOK HANDLE +// HICON HANDLE +// HINSTANCE HANDLE +// HKEY HANDLE +// HKL HANDLE +// HLOCAL HANDLE +// HMENU HANDLE +// HMETAFILE HANDLE +// HMODULE HANDLE +// HPALETTE HANDLE +// HPEN HANDLE +// HRESULT int32 +// HRGN HANDLE +// HSZ HANDLE +// HWINSTA HANDLE +// HWND HANDLE +// INT int32 +// INT_PTR uintptr +// INT8 int8 +// INT16 int16 +// INT32 int32 +// INT64 int64 +// LANGID WORD +// LCID DWORD +// LCTYPE DWORD +// LGRPID DWORD +// LONG int32 +// LONGLONG int64 +// LONG_PTR uintptr +// LONG32 int32 +// LONG64 int64 +// LPARAM LONG_PTR +// LPBOOL *BOOL +// LPBYTE *BYTE +// LPCOLORREF *COLORREF +// LPCSTR *int8 +// LPCTSTR LPCWSTR +// LPCVOID unsafe.Pointer +// LPCWSTR *WCHAR +// LPDWORD *DWORD +// LPHANDLE *HANDLE +// LPINT *INT +// LPLONG *LONG +// LPSTR *CHAR +// LPTSTR LPWSTR +// LPVOID unsafe.Pointer +// LPWORD *WORD +// LPWSTR *WCHAR +// LRESULT LONG_PTR +// PBOOL *BOOL +// PBOOLEAN *BOOLEAN +// PBYTE *BYTE +// PCHAR *CHAR +// PCSTR *CHAR +// PCTSTR PCWSTR +// PCWSTR *WCHAR +// PDWORD *DWORD +// PDWORDLONG *DWORDLONG +// PDWORD_PTR *DWORD_PTR +// PDWORD32 *DWORD32 +// PDWORD64 *DWORD64 +// PFLOAT *FLOAT +// PHALF_PTR *HALF_PTR +// PHANDLE *HANDLE +// PHKEY *HKEY +// PINT_PTR *INT_PTR +// PINT8 *INT8 +// PINT16 *INT16 +// PINT32 *INT32 +// PINT64 *INT64 +// PLCID *LCID +// PLONG *LONG +// PLONGLONG *LONGLONG +// PLONG_PTR *LONG_PTR +// PLONG32 *LONG32 +// PLONG64 *LONG64 +// POINTER_32 struct{} // ??? +// POINTER_64 struct{} // ??? +// POINTER_SIGNED uintptr +// POINTER_UNSIGNED uintptr +// PSHORT *SHORT +// PSIZE_T *SIZE_T +// PSSIZE_T *SSIZE_T +// PSTR *CHAR +// PTBYTE *TBYTE +// PTCHAR *TCHAR +// PTSTR PWSTR +// PUCHAR *UCHAR +// PUHALF_PTR *UHALF_PTR +// PUINT *UINT +// PUINT_PTR *UINT_PTR +// PUINT8 *UINT8 +// PUINT16 *UINT16 +// PUINT32 *UINT32 +// PUINT64 *UINT64 +// PULONG *ULONG +// PULONGLONG *ULONGLONG +// PULONG_PTR *ULONG_PTR +// PULONG32 *ULONG32 +// PULONG64 *ULONG64 +// PUSHORT *USHORT +// PVOID unsafe.Pointer +// PWCHAR *WCHAR +// PWORD *WORD +// PWSTR *WCHAR +// QWORD uint64 +// SC_HANDLE HANDLE +// SC_LOCK LPVOID +// SERVICE_STATUS_HANDLE HANDLE +// SHORT int16 +// SIZE_T ULONG_PTR +// SSIZE_T LONG_PTR +// TBYTE WCHAR +// TCHAR WCHAR +// UCHAR uint8 +// UHALF_PTR struct{} // ??? +// UINT uint32 +// UINT_PTR uintptr +// UINT8 uint8 +// UINT16 uint16 +// UINT32 uint32 +// UINT64 uint64 +// ULONG uint32 +// ULONGLONG uint64 +// ULONG_PTR uintptr +// ULONG32 uint32 +// ULONG64 uint64 +// USHORT uint16 +// USN LONGLONG +// WCHAR uint16 +// WORD uint16 +// WPARAM UINT_PTR +type ( + ATOM = uint16 + BOOL = int32 + COLORREF = uint32 + DWM_FRAME_COUNT = uint64 + WORD = uint16 + DWORD = uint32 + HACCEL = HANDLE + HANDLE = uintptr + HBITMAP = HANDLE + HBRUSH = HANDLE + HCURSOR = HANDLE + HDC = HANDLE + HDROP = HANDLE + HDWP = HANDLE + HENHMETAFILE = HANDLE + HFONT = HANDLE + HGDIOBJ = HANDLE + HGLOBAL = HANDLE + HGLRC = HANDLE + HHOOK = HANDLE + HICON = HANDLE + HIMAGELIST = HANDLE + HINSTANCE = HANDLE + HKEY = HANDLE + HKL = HANDLE + HMENU = HANDLE + HMODULE = HANDLE + HMONITOR = HANDLE + HPEN = HANDLE + HRESULT = int32 + HRGN = HANDLE + HRSRC = HANDLE + HTHUMBNAIL = HANDLE + HWND = HANDLE + LPARAM = uintptr + LPCVOID = unsafe.Pointer + LRESULT = uintptr + PVOID = unsafe.Pointer + QPC_TIME = uint64 + ULONG_PTR = uintptr + SIZE_T = ULONG_PTR + WPARAM = uintptr + UINT = uint +) + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162805.aspx +type POINT struct { + X, Y int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162897.aspx +type RECT struct { + Left, Top, Right, Bottom int32 +} + +func (r *RECT) String() string { + return fmt.Sprintf("RECT (%p): Left: %d, Top: %d, Right: %d, Bottom: %d", r, r.Left, r.Top, r.Right, r.Bottom) +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx +type WNDCLASSEX struct { + Size uint32 + Style uint32 + WndProc uintptr + ClsExtra int32 + WndExtra int32 + Instance HINSTANCE + Icon HICON + Cursor HCURSOR + Background HBRUSH + MenuName *uint16 + ClassName *uint16 + IconSm HICON +} + +type TPMPARAMS struct { + CbSize uint32 + RcExclude RECT +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms644958.aspx +type MSG struct { + Hwnd HWND + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt POINT +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-minmaxinfo +type MINMAXINFO struct { + PtReserved POINT + PtMaxSize POINT + PtMaxPosition POINT + PtMinTrackSize POINT + PtMaxTrackSize POINT +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145037.aspx +type LOGFONT struct { + Height int32 + Width int32 + Escapement int32 + Orientation int32 + Weight int32 + Italic byte + Underline byte + StrikeOut byte + CharSet byte + OutPrecision byte + ClipPrecision byte + Quality byte + PitchAndFamily byte + FaceName [LF_FACESIZE]uint16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646839.aspx +type OPENFILENAME struct { + StructSize uint32 + Owner HWND + Instance HINSTANCE + Filter *uint16 + CustomFilter *uint16 + MaxCustomFilter uint32 + FilterIndex uint32 + File *uint16 + MaxFile uint32 + FileTitle *uint16 + MaxFileTitle uint32 + InitialDir *uint16 + Title *uint16 + Flags uint32 + FileOffset uint16 + FileExtension uint16 + DefExt *uint16 + CustData uintptr + FnHook uintptr + TemplateName *uint16 + PvReserved unsafe.Pointer + DwReserved uint32 + FlagsEx uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb773205.aspx +type BROWSEINFO struct { + Owner HWND + Root *uint16 + DisplayName *uint16 + Title *uint16 + Flags uint32 + CallbackFunc uintptr + LParam uintptr + Image int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627.aspx +type VARIANT struct { + VT uint16 // 2 + WReserved1 uint16 // 4 + WReserved2 uint16 // 6 + WReserved3 uint16 // 8 + Val int64 // 16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms221416.aspx +type DISPPARAMS struct { + Rgvarg uintptr + RgdispidNamedArgs uintptr + CArgs uint32 + CNamedArgs uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms221133.aspx +type EXCEPINFO struct { + WCode uint16 + WReserved uint16 + BstrSource *uint16 + BstrDescription *uint16 + BstrHelpFile *uint16 + DwHelpContext uint32 + PvReserved uintptr + PfnDeferredFillIn uintptr + Scode int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145035.aspx +type LOGBRUSH struct { + LbStyle uint32 + LbColor COLORREF + LbHatch uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183565.aspx +type DEVMODE struct { + DmDeviceName [CCHDEVICENAME]uint16 + DmSpecVersion uint16 + DmDriverVersion uint16 + DmSize uint16 + DmDriverExtra uint16 + DmFields uint32 + DmOrientation int16 + DmPaperSize int16 + DmPaperLength int16 + DmPaperWidth int16 + DmScale int16 + DmCopies int16 + DmDefaultSource int16 + DmPrintQuality int16 + DmColor int16 + DmDuplex int16 + DmYResolution int16 + DmTTOption int16 + DmCollate int16 + DmFormName [CCHFORMNAME]uint16 + DmLogPixels uint16 + DmBitsPerPel uint32 + DmPelsWidth uint32 + DmPelsHeight uint32 + DmDisplayFlags uint32 + DmDisplayFrequency uint32 + DmICMMethod uint32 + DmICMIntent uint32 + DmMediaType uint32 + DmDitherType uint32 + DmReserved1 uint32 + DmReserved2 uint32 + DmPanningWidth uint32 + DmPanningHeight uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx +type BITMAPINFOHEADER struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx +type RGBQUAD struct { + RgbBlue byte + RgbGreen byte + RgbRed byte + RgbReserved byte +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx +type BITMAPINFO struct { + BmiHeader BITMAPINFOHEADER + BmiColors *RGBQUAD +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183371.aspx +type BITMAP struct { + BmType int32 + BmWidth int32 + BmHeight int32 + BmWidthBytes int32 + BmPlanes uint16 + BmBitsPixel uint16 + BmBits unsafe.Pointer +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183567.aspx +type DIBSECTION struct { + DsBm BITMAP + DsBmih BITMAPINFOHEADER + DsBitfields [3]uint32 + DshSection HANDLE + DsOffset uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162607.aspx +type ENHMETAHEADER struct { + IType uint32 + NSize uint32 + RclBounds RECT + RclFrame RECT + DSignature uint32 + NVersion uint32 + NBytes uint32 + NRecords uint32 + NHandles uint16 + SReserved uint16 + NDescription uint32 + OffDescription uint32 + NPalEntries uint32 + SzlDevice SIZE + SzlMillimeters SIZE + CbPixelFormat uint32 + OffPixelFormat uint32 + BOpenGL uint32 + SzlMicrometers SIZE +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145106.aspx +type SIZE struct { + CX, CY int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145132.aspx +type TEXTMETRIC struct { + TmHeight int32 + TmAscent int32 + TmDescent int32 + TmInternalLeading int32 + TmExternalLeading int32 + TmAveCharWidth int32 + TmMaxCharWidth int32 + TmWeight int32 + TmOverhang int32 + TmDigitizedAspectX int32 + TmDigitizedAspectY int32 + TmFirstChar uint16 + TmLastChar uint16 + TmDefaultChar uint16 + TmBreakChar uint16 + TmItalic byte + TmUnderlined byte + TmStruckOut byte + TmPitchAndFamily byte + TmCharSet byte +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183574.aspx +type DOCINFO struct { + CbSize int32 + LpszDocName *uint16 + LpszOutput *uint16 + LpszDatatype *uint16 + FwType uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775514.aspx +type NMHDR struct { + HwndFrom HWND + IdFrom uintptr + Code uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774743.aspx +type LVCOLUMN struct { + Mask uint32 + Fmt int32 + Cx int32 + PszText *uint16 + CchTextMax int32 + ISubItem int32 + IImage int32 + IOrder int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774760.aspx +type LVITEM struct { + Mask uint32 + IItem int32 + ISubItem int32 + State uint32 + StateMask uint32 + PszText *uint16 + CchTextMax int32 + IImage int32 + LParam uintptr + IIndent int32 + IGroupId int32 + CColumns uint32 + PuColumns uint32 +} + +type LVFINDINFO struct { + Flags uint32 + PszText *uint16 + LParam uintptr + Pt POINT + VkDirection uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774754.aspx +type LVHITTESTINFO struct { + Pt POINT + Flags uint32 + IItem int32 + ISubItem int32 + IGroup int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774771.aspx +type NMITEMACTIVATE struct { + Hdr NMHDR + IItem int32 + ISubItem int32 + UNewState uint32 + UOldState uint32 + UChanged uint32 + PtAction POINT + LParam uintptr + UKeyFlags uint32 +} + +type NMLVKEYDOWN struct { + Hdr NMHDR + WVKey uint16 + Flags uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774773.aspx +type NMLISTVIEW struct { + Hdr NMHDR + IItem int32 + ISubItem int32 + UNewState uint32 + UOldState uint32 + UChanged uint32 + PtAction POINT + LParam uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb774780.aspx +type NMLVDISPINFO struct { + Hdr NMHDR + Item LVITEM +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507.aspx +type INITCOMMONCONTROLSEX struct { + DwSize uint32 + DwICC uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb760256.aspx +type TOOLINFO struct { + CbSize uint32 + UFlags uint32 + Hwnd HWND + UId uintptr + Rect RECT + Hinst HINSTANCE + LpszText *uint16 + LParam uintptr + LpReserved unsafe.Pointer +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms645604.aspx +type TRACKMOUSEEVENT struct { + CbSize uint32 + DwFlags uint32 + HwndTrack HWND + DwHoverTime uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms534067.aspx +type GdiplusStartupInput struct { + GdiplusVersion uint32 + DebugEventCallback uintptr + SuppressBackgroundThread BOOL + SuppressExternalCodecs BOOL +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms534068.aspx +type GdiplusStartupOutput struct { + NotificationHook uintptr + NotificationUnhook uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162768.aspx +type PAINTSTRUCT struct { + Hdc HDC + FErase BOOL + RcPaint RECT + FRestore BOOL + FIncUpdate BOOL + RgbReserved [32]byte +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa363646.aspx +type EVENTLOGRECORD struct { + Length uint32 + Reserved uint32 + RecordNumber uint32 + TimeGenerated uint32 + TimeWritten uint32 + EventID uint32 + EventType uint16 + NumStrings uint16 + EventCategory uint16 + ReservedFlags uint16 + ClosingRecordNumber uint32 + StringOffset uint32 + UserSidLength uint32 + UserSidOffset uint32 + DataLength uint32 + DataOffset uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms685996.aspx +type SERVICE_STATUS struct { + DwServiceType uint32 + DwCurrentState uint32 + DwControlsAccepted uint32 + DwWin32ExitCode uint32 + DwServiceSpecificExitCode uint32 + DwCheckPoint uint32 + DwWaitHint uint32 +} + +/* ------------------------- + Undocumented API +------------------------- */ + +type ACCENT_STATE DWORD + +const ( + ACCENT_DISABLED ACCENT_STATE = 0 + ACCENT_ENABLE_GRADIENT ACCENT_STATE = 1 + ACCENT_ENABLE_TRANSPARENTGRADIENT ACCENT_STATE = 2 + ACCENT_ENABLE_BLURBEHIND ACCENT_STATE = 3 + ACCENT_ENABLE_ACRYLICBLURBEHIND ACCENT_STATE = 4 // RS4 1803 + ACCENT_ENABLE_HOSTBACKDROP ACCENT_STATE = 5 // RS5 1809 + ACCENT_INVALID_STATE ACCENT_STATE = 6 +) + +type ACCENT_POLICY struct { + AccentState ACCENT_STATE + AccentFlags DWORD + GradientColor DWORD + AnimationId DWORD +} + +type WINDOWCOMPOSITIONATTRIBDATA struct { + Attrib WINDOWCOMPOSITIONATTRIB + PvData PVOID + CbData SIZE_T +} + +type WINDOWCOMPOSITIONATTRIB DWORD + +const ( + WCA_UNDEFINED WINDOWCOMPOSITIONATTRIB = 0 + WCA_NCRENDERING_ENABLED WINDOWCOMPOSITIONATTRIB = 1 + WCA_NCRENDERING_POLICY WINDOWCOMPOSITIONATTRIB = 2 + WCA_TRANSITIONS_FORCEDISABLED WINDOWCOMPOSITIONATTRIB = 3 + WCA_ALLOW_NCPAINT WINDOWCOMPOSITIONATTRIB = 4 + WCA_CAPTION_BUTTON_BOUNDS WINDOWCOMPOSITIONATTRIB = 5 + WCA_NONCLIENT_RTL_LAYOUT WINDOWCOMPOSITIONATTRIB = 6 + WCA_FORCE_ICONIC_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 7 + WCA_EXTENDED_FRAME_BOUNDS WINDOWCOMPOSITIONATTRIB = 8 + WCA_HAS_ICONIC_BITMAP WINDOWCOMPOSITIONATTRIB = 9 + WCA_THEME_ATTRIBUTES WINDOWCOMPOSITIONATTRIB = 10 + WCA_NCRENDERING_EXILED WINDOWCOMPOSITIONATTRIB = 11 + WCA_NCADORNMENTINFO WINDOWCOMPOSITIONATTRIB = 12 + WCA_EXCLUDED_FROM_LIVEPREVIEW WINDOWCOMPOSITIONATTRIB = 13 + WCA_VIDEO_OVERLAY_ACTIVE WINDOWCOMPOSITIONATTRIB = 14 + WCA_FORCE_ACTIVEWINDOW_APPEARANCE WINDOWCOMPOSITIONATTRIB = 15 + WCA_DISALLOW_PEEK WINDOWCOMPOSITIONATTRIB = 16 + WCA_CLOAK WINDOWCOMPOSITIONATTRIB = 17 + WCA_CLOAKED WINDOWCOMPOSITIONATTRIB = 18 + WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19 + WCA_FREEZE_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 20 + WCA_EVER_UNCLOAKED WINDOWCOMPOSITIONATTRIB = 21 + WCA_VISUAL_OWNER WINDOWCOMPOSITIONATTRIB = 22 + WCA_HOLOGRAPHIC WINDOWCOMPOSITIONATTRIB = 23 + WCA_EXCLUDED_FROM_DDA WINDOWCOMPOSITIONATTRIB = 24 + WCA_PASSIVEUPDATEMODE WINDOWCOMPOSITIONATTRIB = 25 + WCA_USEDARKMODECOLORS WINDOWCOMPOSITIONATTRIB = 26 + WCA_CORNER_STYLE WINDOWCOMPOSITIONATTRIB = 27 + WCA_PART_COLOR WINDOWCOMPOSITIONATTRIB = 28 + WCA_DISABLE_MOVESIZE_FEEDBACK WINDOWCOMPOSITIONATTRIB = 29 + WCA_LAST WINDOWCOMPOSITIONATTRIB = 30 +) + +// ------------------------- + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684225.aspx +type MODULEENTRY32 struct { + Size uint32 + ModuleID uint32 + ProcessID uint32 + GlblcntUsage uint32 + ProccntUsage uint32 + ModBaseAddr *uint8 + ModBaseSize uint32 + HModule HMODULE + SzModule [MAX_MODULE_NAME32 + 1]uint16 + SzExePath [MAX_PATH]uint16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx +type FILETIME struct { + DwLowDateTime uint32 + DwHighDateTime uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119.aspx +type COORD struct { + X, Y int16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311.aspx +type SMALL_RECT struct { + Left, Top, Right, Bottom int16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093.aspx +type CONSOLE_SCREEN_BUFFER_INFO struct { + DwSize COORD + DwCursorPosition COORD + WAttributes uint16 + SrWindow SMALL_RECT + DwMaximumWindowSize COORD +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb773244.aspx +type MARGINS struct { + CxLeftWidth, CxRightWidth, CyTopHeight, CyBottomHeight int32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa969500.aspx +type DWM_BLURBEHIND struct { + DwFlags uint32 + fEnable BOOL + hRgnBlur HRGN + fTransitionOnMaximized BOOL +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa969501.aspx +type DWM_PRESENT_PARAMETERS struct { + cbSize uint32 + fQueue BOOL + cRefreshStart DWM_FRAME_COUNT + cBuffer uint32 + fUseSourceRate BOOL + rateSource UNSIGNED_RATIO + cRefreshesPerFrame uint32 + eSampling DWM_SOURCE_FRAME_SAMPLING +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa969502.aspx +type DWM_THUMBNAIL_PROPERTIES struct { + dwFlags uint32 + rcDestination RECT + rcSource RECT + opacity byte + fVisible BOOL + fSourceClientAreaOnly BOOL +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa969503.aspx +type DWM_TIMING_INFO struct { + cbSize uint32 + rateRefresh UNSIGNED_RATIO + qpcRefreshPeriod QPC_TIME + rateCompose UNSIGNED_RATIO + qpcVBlank QPC_TIME + cRefresh DWM_FRAME_COUNT + cDXRefresh uint32 + qpcCompose QPC_TIME + cFrame DWM_FRAME_COUNT + cDXPresent uint32 + cRefreshFrame DWM_FRAME_COUNT + cFrameSubmitted DWM_FRAME_COUNT + cDXPresentSubmitted uint32 + cFrameConfirmed DWM_FRAME_COUNT + cDXPresentConfirmed uint32 + cRefreshConfirmed DWM_FRAME_COUNT + cDXRefreshConfirmed uint32 + cFramesLate DWM_FRAME_COUNT + cFramesOutstanding uint32 + cFrameDisplayed DWM_FRAME_COUNT + qpcFrameDisplayed QPC_TIME + cRefreshFrameDisplayed DWM_FRAME_COUNT + cFrameComplete DWM_FRAME_COUNT + qpcFrameComplete QPC_TIME + cFramePending DWM_FRAME_COUNT + qpcFramePending QPC_TIME + cFramesDisplayed DWM_FRAME_COUNT + cFramesComplete DWM_FRAME_COUNT + cFramesPending DWM_FRAME_COUNT + cFramesAvailable DWM_FRAME_COUNT + cFramesDropped DWM_FRAME_COUNT + cFramesMissed DWM_FRAME_COUNT + cRefreshNextDisplayed DWM_FRAME_COUNT + cRefreshNextPresented DWM_FRAME_COUNT + cRefreshesDisplayed DWM_FRAME_COUNT + cRefreshesPresented DWM_FRAME_COUNT + cRefreshStarted DWM_FRAME_COUNT + cPixelsReceived uint64 + cPixelsDrawn uint64 + cBuffersEmpty DWM_FRAME_COUNT +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd389402.aspx +type MilMatrix3x2D struct { + S_11, S_12, S_21, S_22 float64 + DX, DY float64 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa969505.aspx +type UNSIGNED_RATIO struct { + uiNumerator uint32 + uiDenominator uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms632603.aspx +type CREATESTRUCT struct { + CreateParams uintptr + Instance HINSTANCE + Menu HMENU + Parent HWND + Cy, Cx int32 + Y, X int32 + Style int32 + Name *uint16 + Class *uint16 + dwExStyle uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145065.aspx +type MONITORINFO struct { + CbSize uint32 + RcMonitor RECT + RcWork RECT + DwFlags uint32 +} + +type WINDOWINFO struct { + CbSize DWORD + RcWindow RECT + RcClient RECT + DwStyle DWORD + DwExStyle DWORD + DwWindowStatus DWORD + CxWindowBorders UINT + CyWindowBorders UINT + AtomWindowType ATOM + WCreatorVersion WORD +} + +type MONITOR_DPI_TYPE int32 + +const ( + MDT_EFFECTIVE_DPI MONITOR_DPI_TYPE = 0 + MDT_ANGULAR_DPI MONITOR_DPI_TYPE = 1 + MDT_RAW_DPI MONITOR_DPI_TYPE = 2 + MDT_DEFAULT MONITOR_DPI_TYPE = 0 +) + +func (w *WINDOWINFO) isStyle(style DWORD) bool { + return w.DwStyle&style == style +} + +func (w *WINDOWINFO) IsPopup() bool { + return w.isStyle(WS_POPUP) +} + +func (m *MONITORINFO) Dump() { + fmt.Printf("MONITORINFO (%p)\n", m) + fmt.Printf(" CbSize : %d\n", m.CbSize) + fmt.Printf(" RcMonitor: %s\n", &m.RcMonitor) + fmt.Printf(" RcWork : %s\n", &m.RcWork) + fmt.Printf(" DwFlags : %d\n", m.DwFlags) +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145066.aspx +type MONITORINFOEX struct { + MONITORINFO + SzDevice [CCHDEVICENAME]uint16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd368826.aspx +type PIXELFORMATDESCRIPTOR struct { + Size uint16 + Version uint16 + DwFlags uint32 + IPixelType byte + ColorBits byte + RedBits, RedShift byte + GreenBits, GreenShift byte + BlueBits, BlueShift byte + AlphaBits, AlphaShift byte + AccumBits byte + AccumRedBits byte + AccumGreenBits byte + AccumBlueBits byte + AccumAlphaBits byte + DepthBits, StencilBits byte + AuxBuffers byte + ILayerType byte + Reserved byte + DwLayerMask uint32 + DwVisibleMask uint32 + DwDamageMask uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx +type INPUT struct { + Type uint32 + Mi MOUSEINPUT + Ki KEYBDINPUT + Hi HARDWAREINPUT +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646273(v=vs.85).aspx +type MOUSEINPUT struct { + Dx int32 + Dy int32 + MouseData uint32 + DwFlags uint32 + Time uint32 + DwExtraInfo uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646271(v=vs.85).aspx +type KEYBDINPUT struct { + WVk uint16 + WScan uint16 + DwFlags uint32 + Time uint32 + DwExtraInfo uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646269(v=vs.85).aspx +type HARDWAREINPUT struct { + UMsg uint32 + WParamL uint16 + WParamH uint16 +} + +type KbdInput struct { + typ uint32 + ki KEYBDINPUT +} + +type MouseInput struct { + typ uint32 + mi MOUSEINPUT +} + +type HardwareInput struct { + typ uint32 + hi HARDWAREINPUT +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms724950(v=vs.85).aspx +type SYSTEMTIME struct { + Year uint16 + Month uint16 + DayOfWeek uint16 + Day uint16 + Hour uint16 + Minute uint16 + Second uint16 + Milliseconds uint16 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms644967(v=vs.85).aspx +type KBDLLHOOKSTRUCT struct { + VkCode DWORD + ScanCode DWORD + Flags DWORD + Time DWORD + DwExtraInfo ULONG_PTR +} + +type HOOKPROC func(int, WPARAM, LPARAM) LRESULT + +type WINDOWPLACEMENT struct { + Length uint32 + Flags uint32 + ShowCmd uint32 + PtMinPosition POINT + PtMaxPosition POINT + RcNormalPosition RECT +} + +type SCROLLINFO struct { + CbSize uint32 + FMask uint32 + NMin int32 + NMax int32 + NPage uint32 + NPos int32 + NTrackPos int32 +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/user32.go b/v2/internal/frontend/desktop/windows/winc/w32/user32.go new file mode 100644 index 000000000..707701f5e --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/user32.go @@ -0,0 +1,1277 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +import ( + "fmt" + "runtime" + "syscall" + "unsafe" +) + +var ( + moduser32 = syscall.NewLazyDLL("user32.dll") + + procRegisterClassEx = moduser32.NewProc("RegisterClassExW") + procLoadIcon = moduser32.NewProc("LoadIconW") + procLoadCursor = moduser32.NewProc("LoadCursorW") + procShowWindow = moduser32.NewProc("ShowWindow") + procShowWindowAsync = moduser32.NewProc("ShowWindowAsync") + procUpdateWindow = moduser32.NewProc("UpdateWindow") + procCreateWindowEx = moduser32.NewProc("CreateWindowExW") + procFindWindowW = moduser32.NewProc("FindWindowW") + procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect") + procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx") + procDestroyWindow = moduser32.NewProc("DestroyWindow") + procDefWindowProc = moduser32.NewProc("DefWindowProcW") + procDefDlgProc = moduser32.NewProc("DefDlgProcW") + procPostQuitMessage = moduser32.NewProc("PostQuitMessage") + procGetMessage = moduser32.NewProc("GetMessageW") + procTranslateMessage = moduser32.NewProc("TranslateMessage") + procDispatchMessage = moduser32.NewProc("DispatchMessageW") + procSendMessage = moduser32.NewProc("SendMessageW") + procPostMessage = moduser32.NewProc("PostMessageW") + procWaitMessage = moduser32.NewProc("WaitMessage") + procSetWindowText = moduser32.NewProc("SetWindowTextW") + procGetWindowTextLength = moduser32.NewProc("GetWindowTextLengthW") + procGetWindowText = moduser32.NewProc("GetWindowTextW") + procGetWindowRect = moduser32.NewProc("GetWindowRect") + procGetWindowInfo = moduser32.NewProc("GetWindowInfo") + procSetWindowCompositionAttribute = moduser32.NewProc("SetWindowCompositionAttribute") + procMoveWindow = moduser32.NewProc("MoveWindow") + procScreenToClient = moduser32.NewProc("ScreenToClient") + procCallWindowProc = moduser32.NewProc("CallWindowProcW") + procSetWindowLong = moduser32.NewProc("SetWindowLongW") + procSetWindowLongPtr = moduser32.NewProc("SetWindowLongW") + procGetWindowLong = moduser32.NewProc("GetWindowLongW") + procGetWindowLongPtr = moduser32.NewProc("GetWindowLongW") + procEnableWindow = moduser32.NewProc("EnableWindow") + procIsWindowEnabled = moduser32.NewProc("IsWindowEnabled") + procIsWindowVisible = moduser32.NewProc("IsWindowVisible") + procSetFocus = moduser32.NewProc("SetFocus") + procGetFocus = moduser32.NewProc("GetFocus") + procSetActiveWindow = moduser32.NewProc("SetActiveWindow") + procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow") + procBringWindowToTop = moduser32.NewProc("BringWindowToTop") + procInvalidateRect = moduser32.NewProc("InvalidateRect") + procGetClientRect = moduser32.NewProc("GetClientRect") + procGetDC = moduser32.NewProc("GetDC") + procReleaseDC = moduser32.NewProc("ReleaseDC") + procSetCapture = moduser32.NewProc("SetCapture") + procReleaseCapture = moduser32.NewProc("ReleaseCapture") + procGetWindowThreadProcessId = moduser32.NewProc("GetWindowThreadProcessId") + procMessageBox = moduser32.NewProc("MessageBoxW") + procGetSystemMetrics = moduser32.NewProc("GetSystemMetrics") + procPostThreadMessageW = moduser32.NewProc("PostThreadMessageW") + procRegisterWindowMessageA = moduser32.NewProc("RegisterWindowMessageA") + //procSysColorBrush = moduser32.NewProc("GetSysColorBrush") + procCopyRect = moduser32.NewProc("CopyRect") + procEqualRect = moduser32.NewProc("EqualRect") + procInflateRect = moduser32.NewProc("InflateRect") + procIntersectRect = moduser32.NewProc("IntersectRect") + procIsRectEmpty = moduser32.NewProc("IsRectEmpty") + procOffsetRect = moduser32.NewProc("OffsetRect") + procPtInRect = moduser32.NewProc("PtInRect") + procSetRect = moduser32.NewProc("SetRect") + procSetRectEmpty = moduser32.NewProc("SetRectEmpty") + procSubtractRect = moduser32.NewProc("SubtractRect") + procUnionRect = moduser32.NewProc("UnionRect") + procCreateDialogParam = moduser32.NewProc("CreateDialogParamW") + procDialogBoxParam = moduser32.NewProc("DialogBoxParamW") + procGetDlgItem = moduser32.NewProc("GetDlgItem") + procDrawIcon = moduser32.NewProc("DrawIcon") + procCreateMenu = moduser32.NewProc("CreateMenu") + //procSetMenu = moduser32.NewProc("SetMenu") + procDestroyMenu = moduser32.NewProc("DestroyMenu") + procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") + procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") + //procDrawMenuBar = moduser32.NewProc("DrawMenuBar") + //procInsertMenuItem = moduser32.NewProc("InsertMenuItemW") // FIXIT: + + procClientToScreen = moduser32.NewProc("ClientToScreen") + procIsDialogMessage = moduser32.NewProc("IsDialogMessageW") + procIsWindow = moduser32.NewProc("IsWindow") + procEndDialog = moduser32.NewProc("EndDialog") + procPeekMessage = moduser32.NewProc("PeekMessageW") + procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW") + procSetWindowPos = moduser32.NewProc("SetWindowPos") + procFillRect = moduser32.NewProc("FillRect") + procDrawText = moduser32.NewProc("DrawTextW") + procAddClipboardFormatListener = moduser32.NewProc("AddClipboardFormatListener") + procRemoveClipboardFormatListener = moduser32.NewProc("RemoveClipboardFormatListener") + procOpenClipboard = moduser32.NewProc("OpenClipboard") + procCloseClipboard = moduser32.NewProc("CloseClipboard") + procEnumClipboardFormats = moduser32.NewProc("EnumClipboardFormats") + procGetClipboardData = moduser32.NewProc("GetClipboardData") + procSetClipboardData = moduser32.NewProc("SetClipboardData") + procEmptyClipboard = moduser32.NewProc("EmptyClipboard") + procGetClipboardFormatName = moduser32.NewProc("GetClipboardFormatNameW") + procIsClipboardFormatAvailable = moduser32.NewProc("IsClipboardFormatAvailable") + procBeginPaint = moduser32.NewProc("BeginPaint") + procEndPaint = moduser32.NewProc("EndPaint") + procGetKeyboardState = moduser32.NewProc("GetKeyboardState") + procMapVirtualKey = moduser32.NewProc("MapVirtualKeyExW") + procGetAsyncKeyState = moduser32.NewProc("GetAsyncKeyState") + procToAscii = moduser32.NewProc("ToAscii") + procSwapMouseButton = moduser32.NewProc("SwapMouseButton") + procGetCursorPos = moduser32.NewProc("GetCursorPos") + procSetCursorPos = moduser32.NewProc("SetCursorPos") + procSetCursor = moduser32.NewProc("SetCursor") + procCreateIcon = moduser32.NewProc("CreateIcon") + procDestroyIcon = moduser32.NewProc("DestroyIcon") + procMonitorFromPoint = moduser32.NewProc("MonitorFromPoint") + procMonitorFromRect = moduser32.NewProc("MonitorFromRect") + procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow") + procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW") + procGetDpiForSystem = moduser32.NewProc("GetDpiForSystem") + procGetDpiForWindow = moduser32.NewProc("GetDpiForWindow") + procEnumDisplayMonitors = moduser32.NewProc("EnumDisplayMonitors") + procEnumDisplaySettingsEx = moduser32.NewProc("EnumDisplaySettingsExW") + procChangeDisplaySettingsEx = moduser32.NewProc("ChangeDisplaySettingsExW") + procSendInput = moduser32.NewProc("SendInput") + procSetWindowsHookEx = moduser32.NewProc("SetWindowsHookExW") + procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx") + procCallNextHookEx = moduser32.NewProc("CallNextHookEx") + + libuser32, _ = syscall.LoadLibrary("user32.dll") + insertMenuItem, _ = syscall.GetProcAddress(libuser32, "InsertMenuItemW") + setMenuItemInfo, _ = syscall.GetProcAddress(libuser32, "SetMenuItemInfoW") + setMenu, _ = syscall.GetProcAddress(libuser32, "SetMenu") + drawMenuBar, _ = syscall.GetProcAddress(libuser32, "DrawMenuBar") + trackPopupMenuEx, _ = syscall.GetProcAddress(libuser32, "TrackPopupMenuEx") + getKeyState, _ = syscall.GetProcAddress(libuser32, "GetKeyState") + getSysColorBrush, _ = syscall.GetProcAddress(libuser32, "GetSysColorBrush") + + getWindowPlacement, _ = syscall.GetProcAddress(libuser32, "GetWindowPlacement") + setWindowPlacement, _ = syscall.GetProcAddress(libuser32, "SetWindowPlacement") + + setScrollInfo, _ = syscall.GetProcAddress(libuser32, "SetScrollInfo") + getScrollInfo, _ = syscall.GetProcAddress(libuser32, "GetScrollInfo") + + mainThread HANDLE +) + +func init() { + runtime.LockOSThread() + mainThread = GetCurrentThreadId() +} + +func GET_X_LPARAM(lp uintptr) int32 { + return int32(int16(LOWORD(uint32(lp)))) +} + +func GET_Y_LPARAM(lp uintptr) int32 { + return int32(int16(HIWORD(uint32(lp)))) +} + +func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { + ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx))) + return ATOM(ret) +} + +func LoadIcon(instance HINSTANCE, iconName *uint16) HICON { + ret, _, _ := procLoadIcon.Call( + uintptr(instance), + uintptr(unsafe.Pointer(iconName))) + + return HICON(ret) +} + +func LoadIconWithResourceID(instance HINSTANCE, res uint16) HICON { + ret, _, _ := procLoadIcon.Call( + uintptr(instance), + uintptr(res)) + + return HICON(ret) +} + +func LoadCursor(instance HINSTANCE, cursorName *uint16) HCURSOR { + ret, _, _ := procLoadCursor.Call( + uintptr(instance), + uintptr(unsafe.Pointer(cursorName))) + + return HCURSOR(ret) +} + +func LoadCursorWithResourceID(instance HINSTANCE, res uint16) HCURSOR { + ret, _, _ := procLoadCursor.Call( + uintptr(instance), + uintptr(res)) + + return HCURSOR(ret) +} + +func ShowWindow(hwnd HWND, cmdshow int) bool { + ret, _, _ := procShowWindow.Call( + uintptr(hwnd), + uintptr(cmdshow)) + + return ret != 0 +} + +func ShowWindowAsync(hwnd HWND, cmdshow int) bool { + ret, _, _ := procShowWindowAsync.Call( + uintptr(hwnd), + uintptr(cmdshow)) + + return ret != 0 +} + +func UpdateWindow(hwnd HWND) bool { + ret, _, _ := procUpdateWindow.Call( + uintptr(hwnd)) + return ret != 0 +} + +func PostThreadMessage(threadID HANDLE, msg int, wp, lp uintptr) { + procPostThreadMessageW.Call(threadID, uintptr(msg), wp, lp) +} + +func RegisterWindowMessage(name *uint16) uint32 { + ret, _, _ := procRegisterWindowMessageA.Call( + uintptr(unsafe.Pointer(name))) + + return uint32(ret) +} + +func PostMainThreadMessage(msg uint32, wp, lp uintptr) bool { + ret, _, _ := procPostThreadMessageW.Call(mainThread, uintptr(msg), wp, lp) + return ret != 0 +} + +func CreateWindowEx(exStyle uint, className, windowName *uint16, + style uint, x, y, width, height int, parent HWND, menu HMENU, + instance HINSTANCE, param unsafe.Pointer) HWND { + ret, _, _ := procCreateWindowEx.Call( + uintptr(exStyle), + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName)), + uintptr(style), + uintptr(x), + uintptr(y), + uintptr(width), + uintptr(height), + uintptr(parent), + uintptr(menu), + uintptr(instance), + uintptr(param)) + + return HWND(ret) +} + +func FindWindowW(className, windowName *uint16) HWND { + ret, _, _ := procFindWindowW.Call( + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName))) + + return HWND(ret) +} + +func AdjustWindowRectEx(rect *RECT, style uint, menu bool, exStyle uint) bool { + ret, _, _ := procAdjustWindowRectEx.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(style), + uintptr(BoolToBOOL(menu)), + uintptr(exStyle)) + + return ret != 0 +} + +func AdjustWindowRect(rect *RECT, style uint, menu bool) bool { + ret, _, _ := procAdjustWindowRect.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(style), + uintptr(BoolToBOOL(menu))) + + return ret != 0 +} + +func DestroyWindow(hwnd HWND) bool { + ret, _, _ := procDestroyWindow.Call(hwnd) + return ret != 0 +} + +func HasGetDpiForWindowFunc() bool { + err := procGetDpiForWindow.Find() + return err == nil +} + +func GetDpiForWindow(hwnd HWND) UINT { + dpi, _, _ := procGetDpiForWindow.Call(hwnd) + return uint(dpi) +} + +func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool { + if procSetWindowCompositionAttribute != nil { + ret, _, _ := procSetWindowCompositionAttribute.Call( + hwnd, + uintptr(unsafe.Pointer(data)), + ) + return ret != 0 + } + return false +} + +func DefWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procDefWindowProc.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func DefDlgProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procDefDlgProc.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func PostQuitMessage(exitCode int) { + procPostQuitMessage.Call( + uintptr(exitCode)) +} + +func GetMessage(msg *MSG, hwnd HWND, msgFilterMin, msgFilterMax uint32) int { + ret, _, _ := procGetMessage.Call( + uintptr(unsafe.Pointer(msg)), + uintptr(hwnd), + uintptr(msgFilterMin), + uintptr(msgFilterMax)) + + return int(ret) +} + +func TranslateMessage(msg *MSG) bool { + ret, _, _ := procTranslateMessage.Call( + uintptr(unsafe.Pointer(msg))) + + return ret != 0 + +} + +func DispatchMessage(msg *MSG) uintptr { + ret, _, _ := procDispatchMessage.Call( + uintptr(unsafe.Pointer(msg))) + + return ret + +} + +func SendMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procSendMessage.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) bool { + ret, _, _ := procPostMessage.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret != 0 +} + +func WaitMessage() bool { + ret, _, _ := procWaitMessage.Call() + return ret != 0 +} + +func SetWindowText(hwnd HWND, text string) { + procSetWindowText.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text)))) +} + +func GetWindowTextLength(hwnd HWND) int { + ret, _, _ := procGetWindowTextLength.Call( + uintptr(hwnd)) + + return int(ret) +} + +func GetWindowInfo(hwnd HWND, info *WINDOWINFO) int { + ret, _, _ := procGetWindowInfo.Call( + hwnd, + uintptr(unsafe.Pointer(info)), + ) + return int(ret) +} + +func GetWindowText(hwnd HWND) string { + textLen := GetWindowTextLength(hwnd) + 1 + + buf := make([]uint16, textLen) + procGetWindowText.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(textLen)) + + return syscall.UTF16ToString(buf) +} + +func GetWindowRect(hwnd HWND) *RECT { + var rect RECT + procGetWindowRect.Call( + hwnd, + uintptr(unsafe.Pointer(&rect))) + + return &rect +} + +func MoveWindow(hwnd HWND, x, y, width, height int, repaint bool) bool { + ret, _, _ := procMoveWindow.Call( + uintptr(hwnd), + uintptr(x), + uintptr(y), + uintptr(width), + uintptr(height), + uintptr(BoolToBOOL(repaint))) + + return ret != 0 + +} + +func ScreenToClient(hwnd HWND, x, y int) (X, Y int, ok bool) { + pt := POINT{X: int32(x), Y: int32(y)} + ret, _, _ := procScreenToClient.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(&pt))) + + return int(pt.X), int(pt.Y), ret != 0 +} + +func CallWindowProc(preWndProc uintptr, hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procCallWindowProc.Call( + preWndProc, + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func SetWindowLong(hwnd HWND, index int, value uint32) uint32 { + ret, _, _ := procSetWindowLong.Call( + uintptr(hwnd), + uintptr(index), + uintptr(value)) + + return uint32(ret) +} + +func SetWindowLongPtr(hwnd HWND, index int, value uintptr) uintptr { + ret, _, _ := procSetWindowLongPtr.Call( + uintptr(hwnd), + uintptr(index), + value) + + return ret +} + +func GetWindowLong(hwnd HWND, index int) int32 { + ret, _, _ := procGetWindowLong.Call( + uintptr(hwnd), + uintptr(index)) + + return int32(ret) +} + +func GetWindowLongPtr(hwnd HWND, index int) uintptr { + ret, _, _ := procGetWindowLongPtr.Call( + uintptr(hwnd), + uintptr(index)) + + return ret +} + +func EnableWindow(hwnd HWND, b bool) bool { + ret, _, _ := procEnableWindow.Call( + uintptr(hwnd), + uintptr(BoolToBOOL(b))) + return ret != 0 +} + +func IsWindowEnabled(hwnd HWND) bool { + ret, _, _ := procIsWindowEnabled.Call( + uintptr(hwnd)) + + return ret != 0 +} + +func IsWindowVisible(hwnd HWND) bool { + ret, _, _ := procIsWindowVisible.Call( + uintptr(hwnd)) + + return ret != 0 +} + +func SetFocus(hwnd HWND) HWND { + ret, _, _ := procSetFocus.Call( + uintptr(hwnd)) + + return HWND(ret) +} + +func SetActiveWindow(hwnd HWND) HWND { + ret, _, _ := procSetActiveWindow.Call( + uintptr(hwnd)) + + return HWND(ret) +} + +func BringWindowToTop(hwnd HWND) bool { + ret, _, _ := procBringWindowToTop.Call(uintptr(hwnd)) + return ret != 0 +} + +func SetForegroundWindow(hwnd HWND) HWND { + ret, _, _ := procSetForegroundWindow.Call( + uintptr(hwnd)) + + return HWND(ret) +} + +func GetFocus() HWND { + ret, _, _ := procGetFocus.Call() + return HWND(ret) +} + +func InvalidateRect(hwnd HWND, rect *RECT, erase bool) bool { + ret, _, _ := procInvalidateRect.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(rect)), + uintptr(BoolToBOOL(erase))) + + return ret != 0 +} + +func GetClientRect(hwnd HWND) *RECT { + var rect RECT + ret, _, _ := procGetClientRect.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(&rect))) + + if ret == 0 { + panic(fmt.Sprintf("GetClientRect(%d) failed", hwnd)) + } + + return &rect +} + +func GetDC(hwnd HWND) HDC { + ret, _, _ := procGetDC.Call( + uintptr(hwnd)) + + return HDC(ret) +} + +func ReleaseDC(hwnd HWND, hDC HDC) bool { + ret, _, _ := procReleaseDC.Call( + uintptr(hwnd), + uintptr(hDC)) + + return ret != 0 +} + +func SetCapture(hwnd HWND) HWND { + ret, _, _ := procSetCapture.Call( + uintptr(hwnd)) + + return HWND(ret) +} + +func ReleaseCapture() bool { + ret, _, _ := procReleaseCapture.Call() + + return ret != 0 +} + +func GetWindowThreadProcessId(hwnd HWND) (HANDLE, int) { + var processId int + ret, _, _ := procGetWindowThreadProcessId.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(&processId))) + + return HANDLE(ret), processId +} + +func MessageBox(hwnd HWND, title, caption string, flags uint) int { + ret, _, _ := procMessageBox.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))), + uintptr(flags)) + + return int(ret) +} + +func GetSystemMetrics(index int) int { + ret, _, _ := procGetSystemMetrics.Call( + uintptr(index)) + + return int(ret) +} + +func GetSysColorBrush(nIndex int) HBRUSH { + /* + ret, _, _ := procSysColorBrush.Call(1, + uintptr(nIndex), + 0, + 0) + + return HBRUSH(ret) + */ + ret, _, _ := syscall.SyscallN(getSysColorBrush, + uintptr(nIndex), + 0, + 0) + + return HBRUSH(ret) +} + +func CopyRect(dst, src *RECT) bool { + ret, _, _ := procCopyRect.Call( + uintptr(unsafe.Pointer(dst)), + uintptr(unsafe.Pointer(src))) + + return ret != 0 +} + +func EqualRect(rect1, rect2 *RECT) bool { + ret, _, _ := procEqualRect.Call( + uintptr(unsafe.Pointer(rect1)), + uintptr(unsafe.Pointer(rect2))) + + return ret != 0 +} + +func InflateRect(rect *RECT, dx, dy int) bool { + ret, _, _ := procInflateRect.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(dx), + uintptr(dy)) + + return ret != 0 +} + +func IntersectRect(dst, src1, src2 *RECT) bool { + ret, _, _ := procIntersectRect.Call( + uintptr(unsafe.Pointer(dst)), + uintptr(unsafe.Pointer(src1)), + uintptr(unsafe.Pointer(src2))) + + return ret != 0 +} + +func IsRectEmpty(rect *RECT) bool { + ret, _, _ := procIsRectEmpty.Call( + uintptr(unsafe.Pointer(rect))) + + return ret != 0 +} + +func OffsetRect(rect *RECT, dx, dy int) bool { + ret, _, _ := procOffsetRect.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(dx), + uintptr(dy)) + + return ret != 0 +} + +func PtInRect(rect *RECT, x, y int) bool { + pt := POINT{X: int32(x), Y: int32(y)} + ret, _, _ := procPtInRect.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(unsafe.Pointer(&pt))) + + return ret != 0 +} + +func SetRect(rect *RECT, left, top, right, bottom int) bool { + ret, _, _ := procSetRect.Call( + uintptr(unsafe.Pointer(rect)), + uintptr(left), + uintptr(top), + uintptr(right), + uintptr(bottom)) + + return ret != 0 +} + +func SetRectEmpty(rect *RECT) bool { + ret, _, _ := procSetRectEmpty.Call( + uintptr(unsafe.Pointer(rect))) + + return ret != 0 +} + +func SubtractRect(dst, src1, src2 *RECT) bool { + ret, _, _ := procSubtractRect.Call( + uintptr(unsafe.Pointer(dst)), + uintptr(unsafe.Pointer(src1)), + uintptr(unsafe.Pointer(src2))) + + return ret != 0 +} + +func UnionRect(dst, src1, src2 *RECT) bool { + ret, _, _ := procUnionRect.Call( + uintptr(unsafe.Pointer(dst)), + uintptr(unsafe.Pointer(src1)), + uintptr(unsafe.Pointer(src2))) + + return ret != 0 +} + +func CreateDialog(hInstance HINSTANCE, lpTemplate *uint16, hWndParent HWND, lpDialogProc uintptr) HWND { + ret, _, _ := procCreateDialogParam.Call( + uintptr(hInstance), + uintptr(unsafe.Pointer(lpTemplate)), + uintptr(hWndParent), + lpDialogProc, + 0) + + return HWND(ret) +} + +func DialogBox(hInstance HINSTANCE, lpTemplateName *uint16, hWndParent HWND, lpDialogProc uintptr) int { + ret, _, _ := procDialogBoxParam.Call( + uintptr(hInstance), + uintptr(unsafe.Pointer(lpTemplateName)), + uintptr(hWndParent), + lpDialogProc, + 0) + + return int(ret) +} + +func GetDlgItem(hDlg HWND, nIDDlgItem int) HWND { + ret, _, _ := procGetDlgItem.Call( + uintptr(unsafe.Pointer(hDlg)), + uintptr(nIDDlgItem)) + + return HWND(ret) +} + +func DrawIcon(hDC HDC, x, y int, hIcon HICON) bool { + ret, _, _ := procDrawIcon.Call( + uintptr(unsafe.Pointer(hDC)), + uintptr(x), + uintptr(y), + uintptr(unsafe.Pointer(hIcon))) + + return ret != 0 +} + +func CreateMenu() HMENU { + ret, _, _ := procCreateMenu.Call(0, + 0, + 0, + 0) + + return HMENU(ret) +} + +func SetMenu(hWnd HWND, hMenu HMENU) bool { + ret, _, _ := syscall.SyscallN(setMenu, + uintptr(hWnd), + uintptr(hMenu)) + return ret != 0 +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-checkmenuradioitem +func SelectRadioMenuItem(menuID uint16, startID uint16, endID uint16, hwnd HWND) bool { + ret, _, _ := procCheckMenuRadioItem.Call( + hwnd, + uintptr(startID), + uintptr(endID), + uintptr(menuID), + MF_BYCOMMAND) + return ret != 0 + +} + +func CreatePopupMenu() HMENU { + ret, _, _ := procCreatePopupMenu.Call(0, + 0, + 0, + 0) + + return HMENU(ret) +} + +func TrackPopupMenuEx(hMenu HMENU, fuFlags uint32, x, y int32, hWnd HWND, lptpm *TPMPARAMS) BOOL { + ret, _, _ := syscall.Syscall6(trackPopupMenuEx, 6, + uintptr(hMenu), + uintptr(fuFlags), + uintptr(x), + uintptr(y), + uintptr(hWnd), + uintptr(unsafe.Pointer(lptpm))) + + return BOOL(ret) +} + +func DrawMenuBar(hWnd HWND) bool { + ret, _, _ := syscall.SyscallN(drawMenuBar, hWnd) + return ret != 0 +} + +func InsertMenuItem(hMenu HMENU, uItem uint32, fByPosition bool, lpmii *MENUITEMINFO) bool { + ret, _, _ := syscall.Syscall6(insertMenuItem, 4, + uintptr(hMenu), + uintptr(uItem), + uintptr(BoolToBOOL(fByPosition)), + uintptr(unsafe.Pointer(lpmii)), + 0, + 0) + + return ret != 0 +} + +func SetMenuItemInfo(hMenu HMENU, uItem uint32, fByPosition bool, lpmii *MENUITEMINFO) bool { + ret, _, _ := syscall.Syscall6(setMenuItemInfo, 4, + uintptr(hMenu), + uintptr(uItem), + uintptr(BoolToBOOL(fByPosition)), + uintptr(unsafe.Pointer(lpmii)), + 0, + 0) + + return ret != 0 +} + +func ClientToScreen(hwnd HWND, x, y int) (int, int) { + pt := POINT{X: int32(x), Y: int32(y)} + + procClientToScreen.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(&pt))) + + return int(pt.X), int(pt.Y) +} + +func IsDialogMessage(hwnd HWND, msg *MSG) bool { + ret, _, _ := procIsDialogMessage.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(msg))) + + return ret != 0 +} + +func IsWindow(hwnd HWND) bool { + ret, _, _ := procIsWindow.Call( + uintptr(hwnd)) + + return ret != 0 +} + +func EndDialog(hwnd HWND, nResult uintptr) bool { + ret, _, _ := procEndDialog.Call( + uintptr(hwnd), + nResult) + + return ret != 0 +} + +func PeekMessage(lpMsg *MSG, hwnd HWND, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool { + ret, _, _ := procPeekMessage.Call( + uintptr(unsafe.Pointer(lpMsg)), + uintptr(hwnd), + uintptr(wMsgFilterMin), + uintptr(wMsgFilterMax), + uintptr(wRemoveMsg)) + + return ret != 0 +} + +func TranslateAccelerator(hwnd HWND, hAccTable HACCEL, lpMsg *MSG) bool { + ret, _, _ := procTranslateMessage.Call( + uintptr(hwnd), + uintptr(hAccTable), + uintptr(unsafe.Pointer(lpMsg))) + + return ret != 0 +} + +func SetWindowPos(hwnd, hWndInsertAfter HWND, x, y, cx, cy int, uFlags uint) bool { + ret, _, _ := procSetWindowPos.Call( + uintptr(hwnd), + uintptr(hWndInsertAfter), + uintptr(x), + uintptr(y), + uintptr(cx), + uintptr(cy), + uintptr(uFlags)) + + return ret != 0 +} + +func FillRect(hDC HDC, lprc *RECT, hbr HBRUSH) bool { + ret, _, _ := procFillRect.Call( + uintptr(hDC), + uintptr(unsafe.Pointer(lprc)), + uintptr(hbr)) + + return ret != 0 +} + +func DrawText(hDC HDC, text string, uCount int, lpRect *RECT, uFormat uint) int { + ret, _, _ := procDrawText.Call( + uintptr(hDC), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))), + uintptr(uCount), + uintptr(unsafe.Pointer(lpRect)), + uintptr(uFormat)) + + return int(ret) +} + +func AddClipboardFormatListener(hwnd HWND) bool { + ret, _, _ := procAddClipboardFormatListener.Call( + uintptr(hwnd)) + return ret != 0 +} + +func RemoveClipboardFormatListener(hwnd HWND) bool { + ret, _, _ := procRemoveClipboardFormatListener.Call( + uintptr(hwnd)) + return ret != 0 +} + +func OpenClipboard(hWndNewOwner HWND) bool { + ret, _, _ := procOpenClipboard.Call( + uintptr(hWndNewOwner)) + return ret != 0 +} + +func CloseClipboard() bool { + ret, _, _ := procCloseClipboard.Call() + return ret != 0 +} + +func EnumClipboardFormats(format uint) uint { + ret, _, _ := procEnumClipboardFormats.Call( + uintptr(format)) + return uint(ret) +} + +func GetClipboardData(uFormat uint) HANDLE { + ret, _, _ := procGetClipboardData.Call( + uintptr(uFormat)) + return HANDLE(ret) +} + +func SetClipboardData(uFormat uint, hMem HANDLE) HANDLE { + ret, _, _ := procSetClipboardData.Call( + uintptr(uFormat), + uintptr(hMem)) + return HANDLE(ret) +} + +func EmptyClipboard() bool { + ret, _, _ := procEmptyClipboard.Call() + return ret != 0 +} + +func GetClipboardFormatName(format uint) (string, bool) { + cchMaxCount := 255 + buf := make([]uint16, cchMaxCount) + ret, _, _ := procGetClipboardFormatName.Call( + uintptr(format), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(cchMaxCount)) + + if ret > 0 { + return syscall.UTF16ToString(buf), true + } + + return "Requested format does not exist or is predefined", false +} + +func IsClipboardFormatAvailable(format uint) bool { + ret, _, _ := procIsClipboardFormatAvailable.Call(uintptr(format)) + return ret != 0 +} + +func BeginPaint(hwnd HWND, paint *PAINTSTRUCT) HDC { + ret, _, _ := procBeginPaint.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(paint))) + return HDC(ret) +} + +func EndPaint(hwnd HWND, paint *PAINTSTRUCT) { + procEndPaint.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(paint))) +} + +func GetKeyboardState(keyState []byte) bool { + if len(keyState) != 256 { + panic("keyState slice must have a size of 256 bytes") + } + ret, _, _ := procGetKeyboardState.Call(uintptr(unsafe.Pointer(&keyState[0]))) + return ret != 0 +} + +func MapVirtualKeyEx(uCode, uMapType uint, dwhkl HKL) uint { + ret, _, _ := procMapVirtualKey.Call( + uintptr(uCode), + uintptr(uMapType), + uintptr(dwhkl)) + return uint(ret) +} + +func GetAsyncKeyState(vKey int) uint16 { + ret, _, _ := procGetAsyncKeyState.Call(uintptr(vKey)) + return uint16(ret) +} + +func ToAscii(uVirtKey, uScanCode uint, lpKeyState *byte, lpChar *uint16, uFlags uint) int { + ret, _, _ := procToAscii.Call( + uintptr(uVirtKey), + uintptr(uScanCode), + uintptr(unsafe.Pointer(lpKeyState)), + uintptr(unsafe.Pointer(lpChar)), + uintptr(uFlags)) + return int(ret) +} + +func SwapMouseButton(fSwap bool) bool { + ret, _, _ := procSwapMouseButton.Call( + uintptr(BoolToBOOL(fSwap))) + return ret != 0 +} + +func GetCursorPos() (x, y int, ok bool) { + pt := POINT{} + ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) + return int(pt.X), int(pt.Y), ret != 0 +} + +func SetCursorPos(x, y int) bool { + ret, _, _ := procSetCursorPos.Call( + uintptr(x), + uintptr(y), + ) + return ret != 0 +} + +func SetCursor(cursor HCURSOR) HCURSOR { + ret, _, _ := procSetCursor.Call( + uintptr(cursor), + ) + return HCURSOR(ret) +} + +func CreateIcon(instance HINSTANCE, nWidth, nHeight int, cPlanes, cBitsPerPixel byte, ANDbits, XORbits *byte) HICON { + ret, _, _ := procCreateIcon.Call( + uintptr(instance), + uintptr(nWidth), + uintptr(nHeight), + uintptr(cPlanes), + uintptr(cBitsPerPixel), + uintptr(unsafe.Pointer(ANDbits)), + uintptr(unsafe.Pointer(XORbits)), + ) + return HICON(ret) +} + +func DestroyIcon(icon HICON) bool { + ret, _, _ := procDestroyIcon.Call( + uintptr(icon), + ) + return ret != 0 +} + +func MonitorFromPoint(x, y int, dwFlags uint32) HMONITOR { + ret, _, _ := procMonitorFromPoint.Call( + uintptr(x), + uintptr(y), + uintptr(dwFlags), + ) + return HMONITOR(ret) +} + +func MonitorFromRect(rc *RECT, dwFlags uint32) HMONITOR { + ret, _, _ := procMonitorFromRect.Call( + uintptr(unsafe.Pointer(rc)), + uintptr(dwFlags), + ) + return HMONITOR(ret) +} + +func MonitorFromWindow(hwnd HWND, dwFlags uint32) HMONITOR { + ret, _, _ := procMonitorFromWindow.Call( + uintptr(hwnd), + uintptr(dwFlags), + ) + return HMONITOR(ret) +} + +func GetMonitorInfo(hMonitor HMONITOR, lmpi *MONITORINFO) bool { + ret, _, _ := procGetMonitorInfo.Call( + uintptr(hMonitor), + uintptr(unsafe.Pointer(lmpi)), + ) + return ret != 0 +} + +func EnumDisplayMonitors(hdc HDC, clip *RECT, fnEnum uintptr, dwData unsafe.Pointer) bool { + ret, _, _ := procEnumDisplayMonitors.Call( + hdc, + uintptr(unsafe.Pointer(clip)), + fnEnum, + uintptr(dwData), + ) + return ret != 0 +} + +func EnumDisplaySettingsEx(szDeviceName *uint16, iModeNum uint32, devMode *DEVMODE, dwFlags uint32) bool { + ret, _, _ := procEnumDisplaySettingsEx.Call( + uintptr(unsafe.Pointer(szDeviceName)), + uintptr(iModeNum), + uintptr(unsafe.Pointer(devMode)), + uintptr(dwFlags), + ) + return ret != 0 +} + +func ChangeDisplaySettingsEx(szDeviceName *uint16, devMode *DEVMODE, hwnd HWND, dwFlags uint32, lParam uintptr) int32 { + ret, _, _ := procChangeDisplaySettingsEx.Call( + uintptr(unsafe.Pointer(szDeviceName)), + uintptr(unsafe.Pointer(devMode)), + uintptr(hwnd), + uintptr(dwFlags), + lParam, + ) + return int32(ret) +} + +/* +func SendInput(inputs []INPUT) uint32 { + var validInputs []C.INPUT + + for _, oneInput := range inputs { + input := C.INPUT{_type: C.DWORD(oneInput.Type)} + + switch oneInput.Type { + case INPUT_MOUSE: + (*MouseInput)(unsafe.Pointer(&input)).mi = oneInput.Mi + case INPUT_KEYBOARD: + (*KbdInput)(unsafe.Pointer(&input)).ki = oneInput.Ki + case INPUT_HARDWARE: + (*HardwareInput)(unsafe.Pointer(&input)).hi = oneInput.Hi + default: + panic("unkown type") + } + + validInputs = append(validInputs, input) + } + + ret, _, _ := procSendInput.Call( + uintptr(len(validInputs)), + uintptr(unsafe.Pointer(&validInputs[0])), + uintptr(unsafe.Sizeof(C.INPUT{})), + ) + return uint32(ret) +}*/ + +func SetWindowsHookEx(idHook int, lpfn HOOKPROC, hMod HINSTANCE, dwThreadId DWORD) HHOOK { + ret, _, _ := procSetWindowsHookEx.Call( + uintptr(idHook), + uintptr(syscall.NewCallback(lpfn)), + uintptr(hMod), + uintptr(dwThreadId), + ) + return HHOOK(ret) +} + +func UnhookWindowsHookEx(hhk HHOOK) bool { + ret, _, _ := procUnhookWindowsHookEx.Call( + uintptr(hhk), + ) + return ret != 0 +} + +func CallNextHookEx(hhk HHOOK, nCode int, wParam WPARAM, lParam LPARAM) LRESULT { + ret, _, _ := procCallNextHookEx.Call( + uintptr(hhk), + uintptr(nCode), + uintptr(wParam), + uintptr(lParam), + ) + return LRESULT(ret) +} + +func GetKeyState(nVirtKey int32) int16 { + ret, _, _ := syscall.SyscallN(getKeyState, + uintptr(nVirtKey)) + return int16(ret) +} + +func DestroyMenu(hMenu HMENU) bool { + ret, _, _ := procDestroyMenu.Call(1, + uintptr(hMenu), + 0, + 0) + + return ret != 0 +} + +func GetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { + ret, _, _ := syscall.SyscallN(getWindowPlacement, + hWnd, + uintptr(unsafe.Pointer(lpwndpl))) + return ret != 0 +} + +func SetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { + ret, _, _ := syscall.SyscallN(setWindowPlacement, + hWnd, + uintptr(unsafe.Pointer(lpwndpl)), + 0) + + return ret != 0 +} + +func SetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO, fRedraw bool) int32 { + ret, _, _ := syscall.Syscall6(setScrollInfo, 4, + hwnd, + uintptr(fnBar), + uintptr(unsafe.Pointer(lpsi)), + uintptr(BoolToBOOL(fRedraw)), + 0, + 0) + + return int32(ret) +} + +func GetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO) bool { + ret, _, _ := syscall.SyscallN(getScrollInfo, + hwnd, + uintptr(fnBar), + uintptr(unsafe.Pointer(lpsi))) + + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/utils.go b/v2/internal/frontend/desktop/windows/winc/w32/utils.go new file mode 100644 index 000000000..4568b4849 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/utils.go @@ -0,0 +1,230 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +import ( + "syscall" + "unicode/utf16" + "unsafe" +) + +func MustLoadLibrary(name string) uintptr { + lib, err := syscall.LoadLibrary(name) + if err != nil { + panic(err) + } + + return uintptr(lib) +} + +func MustGetProcAddress(lib uintptr, name string) uintptr { + addr, err := syscall.GetProcAddress(syscall.Handle(lib), name) + if err != nil { + panic(err) + } + + return uintptr(addr) +} + +func SUCCEEDED(hr HRESULT) bool { + return hr >= 0 +} + +func FAILED(hr HRESULT) bool { + return hr < 0 +} + +func LOWORD(dw uint32) uint16 { + return uint16(dw) +} + +func HIWORD(dw uint32) uint16 { + return uint16(dw >> 16 & 0xffff) +} + +func MAKELONG(lo, hi uint16) uint32 { + return uint32(uint32(lo) | ((uint32(hi)) << 16)) +} + +func BoolToBOOL(value bool) BOOL { + if value { + return 1 + } + + return 0 +} + +func UTF16PtrToString(cstr *uint16) string { + if cstr != nil { + us := make([]uint16, 0, 256) + for p := uintptr(unsafe.Pointer(cstr)); ; p += 2 { + u := *(*uint16)(unsafe.Pointer(p)) + if u == 0 { + return string(utf16.Decode(us)) + } + us = append(us, u) + } + } + + return "" +} + +func ComAddRef(unknown *IUnknown) int32 { + ret, _, _ := syscall.SyscallN(unknown.lpVtbl.pAddRef, + uintptr(unsafe.Pointer(unknown)), + 0, + 0) + return int32(ret) +} + +func ComRelease(unknown *IUnknown) int32 { + ret, _, _ := syscall.SyscallN(unknown.lpVtbl.pRelease, + uintptr(unsafe.Pointer(unknown)), + 0, + 0) + return int32(ret) +} + +func ComQueryInterface(unknown *IUnknown, id *GUID) *IDispatch { + var disp *IDispatch + hr, _, _ := syscall.SyscallN(unknown.lpVtbl.pQueryInterface, + uintptr(unsafe.Pointer(unknown)), + uintptr(unsafe.Pointer(id)), + uintptr(unsafe.Pointer(&disp))) + if hr != 0 { + panic("Invoke QieryInterface error.") + } + return disp +} + +func ComGetIDsOfName(disp *IDispatch, names []string) []int32 { + wnames := make([]*uint16, len(names)) + dispid := make([]int32, len(names)) + for i := 0; i < len(names); i++ { + wnames[i] = syscall.StringToUTF16Ptr(names[i]) + } + hr, _, _ := syscall.Syscall6(disp.lpVtbl.pGetIDsOfNames, 6, + uintptr(unsafe.Pointer(disp)), + uintptr(unsafe.Pointer(IID_NULL)), + uintptr(unsafe.Pointer(&wnames[0])), + uintptr(len(names)), + uintptr(GetUserDefaultLCID()), + uintptr(unsafe.Pointer(&dispid[0]))) + if hr != 0 { + panic("Invoke GetIDsOfName error.") + } + return dispid +} + +func ComInvoke(disp *IDispatch, dispid int32, dispatch int16, params ...interface{}) (result *VARIANT) { + var dispparams DISPPARAMS + + if dispatch&DISPATCH_PROPERTYPUT != 0 { + dispnames := [1]int32{DISPID_PROPERTYPUT} + dispparams.RgdispidNamedArgs = uintptr(unsafe.Pointer(&dispnames[0])) + dispparams.CNamedArgs = 1 + } + var vargs []VARIANT + if len(params) > 0 { + vargs = make([]VARIANT, len(params)) + for i, v := range params { + //n := len(params)-i-1 + n := len(params) - i - 1 + VariantInit(&vargs[n]) + switch v.(type) { + case bool: + if v.(bool) { + vargs[n] = VARIANT{VT_BOOL, 0, 0, 0, 0xffff} + } else { + vargs[n] = VARIANT{VT_BOOL, 0, 0, 0, 0} + } + case *bool: + vargs[n] = VARIANT{VT_BOOL | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*bool))))} + case byte: + vargs[n] = VARIANT{VT_I1, 0, 0, 0, int64(v.(byte))} + case *byte: + vargs[n] = VARIANT{VT_I1 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*byte))))} + case int16: + vargs[n] = VARIANT{VT_I2, 0, 0, 0, int64(v.(int16))} + case *int16: + vargs[n] = VARIANT{VT_I2 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*int16))))} + case uint16: + vargs[n] = VARIANT{VT_UI2, 0, 0, 0, int64(v.(int16))} + case *uint16: + vargs[n] = VARIANT{VT_UI2 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*uint16))))} + case int, int32: + vargs[n] = VARIANT{VT_UI4, 0, 0, 0, int64(v.(int))} + case *int, *int32: + vargs[n] = VARIANT{VT_I4 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*int))))} + case uint, uint32: + vargs[n] = VARIANT{VT_UI4, 0, 0, 0, int64(v.(uint))} + case *uint, *uint32: + vargs[n] = VARIANT{VT_UI4 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*uint))))} + case int64: + vargs[n] = VARIANT{VT_I8, 0, 0, 0, v.(int64)} + case *int64: + vargs[n] = VARIANT{VT_I8 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*int64))))} + case uint64: + vargs[n] = VARIANT{VT_UI8, 0, 0, 0, int64(v.(uint64))} + case *uint64: + vargs[n] = VARIANT{VT_UI8 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*uint64))))} + case float32: + vargs[n] = VARIANT{VT_R4, 0, 0, 0, int64(v.(float32))} + case *float32: + vargs[n] = VARIANT{VT_R4 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*float32))))} + case float64: + vargs[n] = VARIANT{VT_R8, 0, 0, 0, int64(v.(float64))} + case *float64: + vargs[n] = VARIANT{VT_R8 | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*float64))))} + case string: + vargs[n] = VARIANT{VT_BSTR, 0, 0, 0, int64(uintptr(unsafe.Pointer(SysAllocString(v.(string)))))} + case *string: + vargs[n] = VARIANT{VT_BSTR | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*string))))} + case *IDispatch: + vargs[n] = VARIANT{VT_DISPATCH, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*IDispatch))))} + case **IDispatch: + vargs[n] = VARIANT{VT_DISPATCH | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(**IDispatch))))} + case nil: + vargs[n] = VARIANT{VT_NULL, 0, 0, 0, 0} + case *VARIANT: + vargs[n] = VARIANT{VT_VARIANT | VT_BYREF, 0, 0, 0, int64(uintptr(unsafe.Pointer(v.(*VARIANT))))} + default: + panic("unknown type") + } + } + dispparams.Rgvarg = uintptr(unsafe.Pointer(&vargs[0])) + dispparams.CArgs = uint32(len(params)) + } + + var ret VARIANT + var excepInfo EXCEPINFO + VariantInit(&ret) + hr, _, _ := syscall.Syscall9(disp.lpVtbl.pInvoke, 8, + uintptr(unsafe.Pointer(disp)), + uintptr(dispid), + uintptr(unsafe.Pointer(IID_NULL)), + uintptr(GetUserDefaultLCID()), + uintptr(dispatch), + uintptr(unsafe.Pointer(&dispparams)), + uintptr(unsafe.Pointer(&ret)), + uintptr(unsafe.Pointer(&excepInfo)), + 0) + if hr != 0 { + if excepInfo.BstrDescription != nil { + bs := UTF16PtrToString(excepInfo.BstrDescription) + panic(bs) + } + } + for _, varg := range vargs { + if varg.VT == VT_BSTR && varg.Val != 0 { + SysFreeString(((*int16)(unsafe.Pointer(uintptr(varg.Val))))) + } + } + result = &ret + return +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go b/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go new file mode 100644 index 000000000..8a14f0cb7 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go @@ -0,0 +1,152 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +import ( + "syscall" + "unsafe" +) + +// LISTVIEW parts +const ( + LVP_LISTITEM = 1 + LVP_LISTGROUP = 2 + LVP_LISTDETAIL = 3 + LVP_LISTSORTEDDETAIL = 4 + LVP_EMPTYTEXT = 5 + LVP_GROUPHEADER = 6 + LVP_GROUPHEADERLINE = 7 + LVP_EXPANDBUTTON = 8 + LVP_COLLAPSEBUTTON = 9 + LVP_COLUMNDETAIL = 10 +) + +// LVP_LISTITEM states +const ( + LISS_NORMAL = 1 + LISS_HOT = 2 + LISS_SELECTED = 3 + LISS_DISABLED = 4 + LISS_SELECTEDNOTFOCUS = 5 + LISS_HOTSELECTED = 6 +) + +// TREEVIEW parts +const ( + TVP_TREEITEM = 1 + TVP_GLYPH = 2 + TVP_BRANCH = 3 + TVP_HOTGLYPH = 4 +) + +// TVP_TREEITEM states +const ( + TREIS_NORMAL = 1 + TREIS_HOT = 2 + TREIS_SELECTED = 3 + TREIS_DISABLED = 4 + TREIS_SELECTEDNOTFOCUS = 5 + TREIS_HOTSELECTED = 6 +) + +type HTHEME HANDLE + +var ( + // Library + libuxtheme uintptr + + // Functions + closeThemeData uintptr + drawThemeBackground uintptr + drawThemeText uintptr + getThemeTextExtent uintptr + openThemeData uintptr + setWindowTheme uintptr +) + +func Init() { + // Library + libuxtheme = MustLoadLibrary("uxtheme.dll") + + // Functions + closeThemeData = MustGetProcAddress(libuxtheme, "CloseThemeData") + drawThemeBackground = MustGetProcAddress(libuxtheme, "DrawThemeBackground") + drawThemeText = MustGetProcAddress(libuxtheme, "DrawThemeText") + getThemeTextExtent = MustGetProcAddress(libuxtheme, "GetThemeTextExtent") + openThemeData = MustGetProcAddress(libuxtheme, "OpenThemeData") + setWindowTheme = MustGetProcAddress(libuxtheme, "SetWindowTheme") +} + +func CloseThemeData(hTheme HTHEME) HRESULT { + ret, _, _ := syscall.SyscallN(closeThemeData, + uintptr(hTheme), + 0, + 0) + + return HRESULT(ret) +} + +func DrawThemeBackground(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pRect, pClipRect *RECT) HRESULT { + ret, _, _ := syscall.Syscall6(drawThemeBackground, 6, + uintptr(hTheme), + uintptr(hdc), + uintptr(iPartId), + uintptr(iStateId), + uintptr(unsafe.Pointer(pRect)), + uintptr(unsafe.Pointer(pClipRect))) + + return HRESULT(ret) +} + +func DrawThemeText(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags, dwTextFlags2 uint32, pRect *RECT) HRESULT { + ret, _, _ := syscall.Syscall9(drawThemeText, 9, + uintptr(hTheme), + uintptr(hdc), + uintptr(iPartId), + uintptr(iStateId), + uintptr(unsafe.Pointer(pszText)), + uintptr(iCharCount), + uintptr(dwTextFlags), + uintptr(dwTextFlags2), + uintptr(unsafe.Pointer(pRect))) + + return HRESULT(ret) +} + +func GetThemeTextExtent(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags uint32, pBoundingRect, pExtentRect *RECT) HRESULT { + ret, _, _ := syscall.Syscall9(getThemeTextExtent, 9, + uintptr(hTheme), + uintptr(hdc), + uintptr(iPartId), + uintptr(iStateId), + uintptr(unsafe.Pointer(pszText)), + uintptr(iCharCount), + uintptr(dwTextFlags), + uintptr(unsafe.Pointer(pBoundingRect)), + uintptr(unsafe.Pointer(pExtentRect))) + + return HRESULT(ret) +} + +func OpenThemeData(hwnd HWND, pszClassList *uint16) HTHEME { + ret, _, _ := syscall.SyscallN(openThemeData, + uintptr(hwnd), + uintptr(unsafe.Pointer(pszClassList)), + 0) + + return HTHEME(ret) +} + +func SetWindowTheme(hwnd HWND, pszSubAppName, pszSubIdList *uint16) HRESULT { + ret, _, _ := syscall.SyscallN(setWindowTheme, + uintptr(hwnd), + uintptr(unsafe.Pointer(pszSubAppName)), + uintptr(unsafe.Pointer(pszSubIdList))) + + return HRESULT(ret) +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/vars.go b/v2/internal/frontend/desktop/windows/winc/w32/vars.go new file mode 100644 index 000000000..cb69f9d19 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/vars.go @@ -0,0 +1,16 @@ +//go:build windows + +/* + * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. + * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. + */ + +package w32 + +var ( + IID_NULL = &GUID{0x00000000, 0x0000, 0x0000, [8]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} + IID_IUnknown = &GUID{0x00000000, 0x0000, 0x0000, [8]byte{0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}} + IID_IDispatch = &GUID{0x00020400, 0x0000, 0x0000, [8]byte{0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}} + IID_IConnectionPointContainer = &GUID{0xB196B284, 0xBAB4, 0x101A, [8]byte{0xB6, 0x9C, 0x00, 0xAA, 0x00, 0x34, 0x1D, 0x07}} + IID_IConnectionPoint = &GUID{0xB196B286, 0xBAB4, 0x101A, [8]byte{0xB6, 0x9C, 0x00, 0xAA, 0x00, 0x34, 0x1D, 0x07}} +) diff --git a/v2/internal/frontend/desktop/windows/winc/w32/wda.go b/v2/internal/frontend/desktop/windows/winc/w32/wda.go new file mode 100644 index 000000000..3925f2805 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/wda.go @@ -0,0 +1,47 @@ +//go:build windows + +package w32 + +import ( + "syscall" + + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" +) + +var user32 = syscall.NewLazyDLL("user32.dll") +var procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity") +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +const ( + WDA_NONE = 0x00000000 + WDA_MONITOR = 0x00000001 + WDA_EXCLUDEFROMCAPTURE = 0x00000011 // windows 10 2004+ +) + +func isWindowsVersionAtLeast(major, minor, build int) bool { + if windowsVersion.Major > major { + return true + } + if windowsVersion.Major < major { + return false + } + if windowsVersion.Minor > minor { + return true + } + if windowsVersion.Minor < minor { + return false + } + return windowsVersion.Build >= build +} + +func SetWindowDisplayAffinity(hwnd uintptr, affinity uint32) bool { + if affinity == WDA_EXCLUDEFROMCAPTURE && !isWindowsVersionAtLeast(10, 0, 19041) { + // for older windows versions, use WDA_MONITOR + affinity = WDA_MONITOR + } + ret, _, _ := procSetWindowDisplayAffinity.Call( + hwnd, + uintptr(affinity), + ) + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/winc/wndproc.go b/v2/internal/frontend/desktop/windows/winc/wndproc.go new file mode 100644 index 000000000..3db1652c3 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/wndproc.go @@ -0,0 +1,154 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package winc + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" +) + +var wmInvokeCallback uint32 + +func init() { + wmInvokeCallback = RegisterWindowMessage("WincV0.InvokeCallback") +} + +func genPoint(p uintptr) (x, y int) { + x = int(w32.LOWORD(uint32(p))) + y = int(w32.HIWORD(uint32(p))) + return +} + +func genMouseEventArg(wparam, lparam uintptr) *MouseEventData { + var data MouseEventData + data.Button = int(wparam) + data.X, data.Y = genPoint(lparam) + + return &data +} + +func genDropFilesEventArg(wparam uintptr) *DropFilesEventData { + hDrop := w32.HDROP(wparam) + + var data DropFilesEventData + _, fileCount := w32.DragQueryFile(hDrop, 0xFFFFFFFF) + data.Files = make([]string, fileCount) + + var i uint + for i = 0; i < fileCount; i++ { + data.Files[i], _ = w32.DragQueryFile(hDrop, i) + } + + data.X, data.Y, _ = w32.DragQueryPoint(hDrop) + w32.DragFinish(hDrop) + return &data +} + +func generalWndProc(hwnd w32.HWND, msg uint32, wparam, lparam uintptr) uintptr { + + switch msg { + case w32.WM_HSCROLL: + //println("case w32.WM_HSCROLL") + + case w32.WM_VSCROLL: + //println("case w32.WM_VSCROLL") + } + + if controller := GetMsgHandler(hwnd); controller != nil { + ret := controller.WndProc(msg, wparam, lparam) + + switch msg { + case w32.WM_NOTIFY: //Reflect notification to control + nm := (*w32.NMHDR)(unsafe.Pointer(lparam)) + if controller := GetMsgHandler(nm.HwndFrom); controller != nil { + ret := controller.WndProc(msg, wparam, lparam) + if ret != 0 { + w32.SetWindowLong(hwnd, w32.DWL_MSGRESULT, uint32(ret)) + return w32.TRUE + } + } + case w32.WM_COMMAND: + if lparam != 0 { //Reflect message to control + h := w32.HWND(lparam) + if controller := GetMsgHandler(h); controller != nil { + ret := controller.WndProc(msg, wparam, lparam) + if ret != 0 { + w32.SetWindowLong(hwnd, w32.DWL_MSGRESULT, uint32(ret)) + return w32.TRUE + } + } + } + case w32.WM_CLOSE: + controller.OnClose().Fire(NewEvent(controller, nil)) + case w32.WM_KILLFOCUS: + controller.OnKillFocus().Fire(NewEvent(controller, nil)) + case w32.WM_SETFOCUS: + controller.OnSetFocus().Fire(NewEvent(controller, nil)) + case w32.WM_DROPFILES: + controller.OnDropFiles().Fire(NewEvent(controller, genDropFilesEventArg(wparam))) + case w32.WM_CONTEXTMENU: + if wparam != 0 { //Reflect message to control + h := w32.HWND(wparam) + if controller := GetMsgHandler(h); controller != nil { + contextMenu := controller.ContextMenu() + x, y := genPoint(lparam) + + if contextMenu != nil { + id := w32.TrackPopupMenuEx( + contextMenu.hMenu, + w32.TPM_NOANIMATION|w32.TPM_RETURNCMD, + int32(x), + int32(y), + controller.Handle(), + nil) + + item := findMenuItemByID(int(id)) + if item != nil { + item.OnClick().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + } + return 0 + } + } + } + + case w32.WM_LBUTTONDOWN: + controller.OnLBDown().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_LBUTTONUP: + controller.OnLBUp().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_LBUTTONDBLCLK: + controller.OnLBDbl().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_MBUTTONDOWN: + controller.OnMBDown().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_MBUTTONUP: + controller.OnMBUp().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_RBUTTONDOWN: + controller.OnRBDown().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_RBUTTONUP: + controller.OnRBUp().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_RBUTTONDBLCLK: + controller.OnRBDbl().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_MOUSEMOVE: + controller.OnMouseMove().Fire(NewEvent(controller, genMouseEventArg(wparam, lparam))) + case w32.WM_PAINT: + canvas := NewCanvasFromHwnd(hwnd) + defer canvas.Dispose() + controller.OnPaint().Fire(NewEvent(controller, &PaintEventData{Canvas: canvas})) + case w32.WM_KEYUP: + controller.OnKeyUp().Fire(NewEvent(controller, &KeyUpEventData{int(wparam), int(lparam)})) + case w32.WM_SIZE: + x, y := genPoint(lparam) + controller.OnSize().Fire(NewEvent(controller, &SizeEventData{uint(wparam), x, y})) + case wmInvokeCallback: + controller.invokeCallbacks() + } + return ret + } + + return w32.DefWindowProc(hwnd, uint32(msg), wparam, lparam) +} diff --git a/v2/internal/frontend/desktop/windows/window.go b/v2/internal/frontend/desktop/windows/window.go new file mode 100644 index 000000000..b04d61814 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/window.go @@ -0,0 +1,367 @@ +//go:build windows + +package windows + +import ( + "sync" + "unsafe" + + "github.com/wailsapp/go-webview2/pkg/edge" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" + winoptions "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +type Window struct { + winc.Form + frontendOptions *options.App + applicationMenu *menu.Menu + minWidth, minHeight, maxWidth, maxHeight int + versionInfo *operatingsystem.WindowsVersionInfo + isDarkMode bool + isActive bool + hasBeenShown bool + + // Theme + theme winoptions.Theme + themeChanged bool + + framelessWithDecorations bool + + OnSuspend func() + OnResume func() + + chromium *edge.Chromium + + // isMinimizing indicates whether the window is currently being minimized + // 标识窗口是否处于最小化状态,用于解决最小化/恢复时的闪屏问题 + // This flag is used to prevent unnecessary redraws during minimize/restore transitions for frameless windows + // 此标志用于防止无边框窗口在最小化/恢复过程中的不必要重绘 + // Reference: https://github.com/wailsapp/wails/issues/3951 + isMinimizing bool +} + +func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *operatingsystem.WindowsVersionInfo, chromium *edge.Chromium) *Window { + windowsOptions := appoptions.Windows + + result := &Window{ + frontendOptions: appoptions, + minHeight: appoptions.MinHeight, + minWidth: appoptions.MinWidth, + maxHeight: appoptions.MaxHeight, + maxWidth: appoptions.MaxWidth, + versionInfo: versionInfo, + isActive: true, + themeChanged: true, + chromium: chromium, + + framelessWithDecorations: appoptions.Frameless && (windowsOptions == nil || !windowsOptions.DisableFramelessWindowDecorations), + } + result.SetIsForm(true) + + var exStyle int + if windowsOptions != nil { + exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW + if windowsOptions.WindowIsTranslucent { + exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP + } + } + if appoptions.AlwaysOnTop { + exStyle |= w32.WS_EX_TOPMOST + } + + var dwStyle = w32.WS_OVERLAPPEDWINDOW + + windowClassName := "wailsWindow" + if windowsOptions != nil && windowsOptions.WindowClassName != "" { + windowClassName = windowsOptions.WindowClassName + } + + winc.RegClassOnlyOnce(windowClassName) + handle := winc.CreateWindow(windowClassName, parent, uint(exStyle), uint(dwStyle)) + result.SetHandle(handle) + winc.RegMsgHandler(result) + result.SetParent(parent) + + loadIcon := true + if windowsOptions != nil && windowsOptions.DisableWindowIcon == true { + loadIcon = false + } + if loadIcon { + if ico, err := winc.NewIconFromResource(winc.GetAppInstance(), uint16(winc.AppIconID)); err == nil { + result.SetIcon(0, ico) + } + } + + if appoptions.BackgroundColour != nil { + win32.SetBackgroundColour(result.Handle(), appoptions.BackgroundColour.R, appoptions.BackgroundColour.G, appoptions.BackgroundColour.B) + } + + if windowsOptions != nil { + result.theme = windowsOptions.Theme + } else { + result.theme = winoptions.SystemDefault + } + + result.SetSize(appoptions.Width, appoptions.Height) + result.SetText(appoptions.Title) + result.EnableSizable(!appoptions.DisableResize) + if !appoptions.Fullscreen { + result.EnableMaxButton(!appoptions.DisableResize) + result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight) + result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) + } + + result.UpdateTheme() + + if windowsOptions != nil { + result.OnSuspend = windowsOptions.OnSuspend + result.OnResume = windowsOptions.OnResume + if windowsOptions.WindowIsTranslucent { + if !win32.SupportsBackdropTypes() { + result.SetTranslucentBackground() + } else { + win32.EnableTranslucency(result.Handle(), win32.BackdropType(windowsOptions.BackdropType)) + } + } + + if windowsOptions.ContentProtection { + w32.SetWindowDisplayAffinity(result.Handle(), w32.WDA_EXCLUDEFROMCAPTURE) + } + + if windowsOptions.DisableWindowIcon { + result.DisableIcon() + } + } + + // Dlg forces display of focus rectangles, as soon as the user starts to type. + w32.SendMessage(result.Handle(), w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) + + result.SetFont(winc.DefaultFont) + + if appoptions.Menu != nil { + result.SetApplicationMenu(appoptions.Menu) + } + + return result +} + +func (w *Window) Fullscreen() { + if w.Form.IsFullScreen() { + return + } + if w.framelessWithDecorations { + win32.ExtendFrameIntoClientArea(w.Handle(), false) + } + w.Form.SetMaxSize(0, 0) + w.Form.SetMinSize(0, 0) + w.Form.Fullscreen() +} + +func (w *Window) UnFullscreen() { + if !w.Form.IsFullScreen() { + return + } + if w.framelessWithDecorations { + win32.ExtendFrameIntoClientArea(w.Handle(), true) + } + w.Form.UnFullscreen() + w.SetMinSize(w.minWidth, w.minHeight) + w.SetMaxSize(w.maxWidth, w.maxHeight) +} + +func (w *Window) Restore() { + if w.Form.IsFullScreen() { + w.UnFullscreen() + } else { + w.Form.Restore() + } +} + +func (w *Window) SetMinSize(minWidth int, minHeight int) { + w.minWidth = minWidth + w.minHeight = minHeight + w.Form.SetMinSize(minWidth, minHeight) +} + +func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { + w.maxWidth = maxWidth + w.maxHeight = maxHeight + w.Form.SetMaxSize(maxWidth, maxHeight) +} + +func (w *Window) IsVisible() bool { + return win32.IsVisible(w.Handle()) +} + +func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + + switch msg { + case win32.WM_POWERBROADCAST: + switch wparam { + case win32.PBT_APMSUSPEND: + if w.OnSuspend != nil { + w.OnSuspend() + } + case win32.PBT_APMRESUMEAUTOMATIC: + if w.OnResume != nil { + w.OnResume() + } + } + case w32.WM_SETTINGCHANGE: + settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam))) + if settingChanged == "ImmersiveColorSet" { + w.themeChanged = true + w.UpdateTheme() + } + return 0 + case w32.WM_NCLBUTTONDOWN: + w32.SetFocus(w.Handle()) + case w32.WM_MOVE, w32.WM_MOVING: + w.chromium.NotifyParentWindowPositionChanged() + case w32.WM_ACTIVATE: + //if !w.frontendOptions.Frameless { + w.themeChanged = true + if int(wparam) == w32.WA_INACTIVE { + w.isActive = false + w.UpdateTheme() + } else { + w.isActive = true + w.UpdateTheme() + //} + } + + case 0x02E0: //w32.WM_DPICHANGED + newWindowSize := (*w32.RECT)(unsafe.Pointer(lparam)) + w32.SetWindowPos(w.Handle(), + uintptr(0), + int(newWindowSize.Left), + int(newWindowSize.Top), + int(newWindowSize.Right-newWindowSize.Left), + int(newWindowSize.Bottom-newWindowSize.Top), + w32.SWP_NOZORDER|w32.SWP_NOACTIVATE) + } + + if w.frontendOptions.Frameless { + switch msg { + case w32.WM_ACTIVATE: + // If we want to have a frameless window but with the default frame decorations, extend the DWM client area. + // This Option is not affected by returning 0 in WM_NCCALCSIZE. + // As a result we have hidden the titlebar but still have the default window frame styling. + // See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks + if w.framelessWithDecorations { + win32.ExtendFrameIntoClientArea(w.Handle(), true) + } + case w32.WM_NCCALCSIZE: + // Disable the standard frame by allowing the client area to take the full + // window size. + // See: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks + // This hides the titlebar and also disables the resizing from user interaction because the standard frame is not + // shown. We still need the WS_THICKFRAME style to enable resizing from the frontend. + if wparam != 0 { + rgrc := (*w32.RECT)(unsafe.Pointer(lparam)) + if w.Form.IsFullScreen() { + // In Full-Screen mode we don't need to adjust anything + w.SetPadding(edge.Rect{}) + } else if w.IsMaximised() { + // If the window is maximized we must adjust the client area to the work area of the monitor. Otherwise + // some content goes beyond the visible part of the monitor. + // Make sure to use the provided RECT to get the monitor, because during maximizig there might be + // a wrong monitor returned in multi screen mode when using MonitorFromWindow. + // See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549 + monitor := w32.MonitorFromRect(rgrc, w32.MONITOR_DEFAULTTONULL) + + var monitorInfo w32.MONITORINFO + monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) + if monitor != 0 && w32.GetMonitorInfo(monitor, &monitorInfo) { + *rgrc = monitorInfo.RcWork + + maxWidth := w.frontendOptions.MaxWidth + maxHeight := w.frontendOptions.MaxHeight + if maxWidth > 0 || maxHeight > 0 { + var dpiX, dpiY uint + w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY) + + maxWidth := int32(winc.ScaleWithDPI(maxWidth, dpiX)) + if maxWidth > 0 && rgrc.Right-rgrc.Left > maxWidth { + rgrc.Right = rgrc.Left + maxWidth + } + + maxHeight := int32(winc.ScaleWithDPI(maxHeight, dpiY)) + if maxHeight > 0 && rgrc.Bottom-rgrc.Top > maxHeight { + rgrc.Bottom = rgrc.Top + maxHeight + } + } + } + w.SetPadding(edge.Rect{}) + } else { + // This is needed to workaround the resize flickering in frameless mode with WindowDecorations + // See: https://stackoverflow.com/a/6558508 + // The workaround originally suggests to decrese the bottom 1px, but that seems to bring up a thin + // white line on some Windows-Versions, due to DrawBackground using also this reduces ClientSize. + // Increasing the bottom also worksaround the flickering but we would loose 1px of the WebView content + // therefore let's pad the content with 1px at the bottom. + rgrc.Bottom += 1 + w.SetPadding(edge.Rect{Bottom: 1}) + } + return 0 + } + } + } + return w.Form.WndProc(msg, wparam, lparam) +} + +func (w *Window) IsMaximised() bool { + return win32.IsWindowMaximised(w.Handle()) +} + +func (w *Window) IsMinimised() bool { + return win32.IsWindowMinimised(w.Handle()) +} + +func (w *Window) IsNormal() bool { + return win32.IsWindowNormal(w.Handle()) +} + +func (w *Window) IsFullScreen() bool { + return win32.IsWindowFullScreen(w.Handle()) +} + +func (w *Window) SetTheme(theme winoptions.Theme) { + w.theme = theme + w.themeChanged = true + w.Invoke(func() { + w.UpdateTheme() + }) +} + +func invokeSync[T any](cba *Window, fn func() (T, error)) (res T, err error) { + var wg sync.WaitGroup + wg.Add(1) + cba.Invoke(func() { + res, err = fn() + wg.Done() + }) + wg.Wait() + return res, err +} + +// SetPadding is a filter that wraps chromium.SetPadding to prevent unnecessary redraws during minimize/restore +// 包装了chromium.SetPadding的过滤器,用于防止窗口最小化/恢复过程中的不必要重绘 +// This fixes window flickering when minimizing/restoring frameless windows +// 这修复了无边框窗口在最小化/恢复时的闪烁问题 +// Reference: https://github.com/wailsapp/wails/issues/3951 +func (w *Window) SetPadding(padding edge.Rect) { + // Skip SetPadding if window is being minimized to prevent flickering + // 如果窗口正在最小化,跳过设置padding以防止闪烁 + if w.isMinimizing { + return + } + w.chromium.SetPadding(padding) +} diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go new file mode 100644 index 000000000..8a130890d --- /dev/null +++ b/v2/internal/frontend/devserver/devserver.go @@ -0,0 +1,319 @@ +//go:build dev +// +build dev + +// Package devserver provides a web-based frontend so that +// it is possible to run a Wails app in a browsers. +package devserver + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "sync" + + "github.com/wailsapp/wails/v2/pkg/assetserver" + + "github.com/wailsapp/wails/v2/internal/frontend/runtime" + + "github.com/gorilla/websocket" + "github.com/labstack/echo/v4" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/menumanager" + "github.com/wailsapp/wails/v2/pkg/options" +) + +type Screen = frontend.Screen + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, +} + +type DevWebServer struct { + server *echo.Echo + ctx context.Context + appoptions *options.App + logger *logger.Logger + appBindings *binding.Bindings + dispatcher frontend.Dispatcher + socketMutex sync.Mutex + websocketClients map[*websocket.Conn]*sync.Mutex + menuManager *menumanager.Manager + starttime string + + // Desktop frontend + frontend.Frontend + + devServerAddr string +} + +func (d *DevWebServer) Run(ctx context.Context) error { + d.ctx = ctx + + d.server.GET("/wails/reload", d.handleReload) + d.server.GET("/wails/ipc", d.handleIPCWebSocket) + + assetServerConfig, err := assetserver.BuildAssetServerConfig(d.appoptions) + if err != nil { + return err + } + + var myLogger assetserver.Logger + if _logger := ctx.Value("logger"); _logger != nil { + myLogger = _logger.(*logger.Logger) + } + + var wsHandler http.Handler + + _fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string) + if _fronendDevServerURL == "" { + assetdir, _ := ctx.Value("assetdir").(string) + d.server.GET("/wails/assetdir", func(c echo.Context) error { + return c.String(http.StatusOK, assetdir) + }) + + } else { + externalURL, err := url.Parse(_fronendDevServerURL) + if err != nil { + return err + } + + // WebSockets aren't currently supported in prod mode, so a WebSocket connection is the result of the + // FrontendDevServer e.g. Vite to support auto reloads. + // Therefore we direct WebSockets directly to the FrontendDevServer instead of returning a NotImplementedStatus. + wsHandler = httputil.NewSingleHostReverseProxy(externalURL) + } + + assetHandler, err := assetserver.NewAssetHandler(assetServerConfig, myLogger) + if err != nil { + log.Fatal(err) + } + + // Setup internal dev server + bindingsJSON, err := d.appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + + assetServer, err := assetserver.NewDevAssetServer(assetHandler, bindingsJSON, ctx.Value("assetdir") != nil, myLogger, runtime.RuntimeAssetsBundle) + if err != nil { + log.Fatal(err) + } + + d.server.Any("/*", func(c echo.Context) error { + if c.IsWebSocket() { + wsHandler.ServeHTTP(c.Response(), c.Request()) + } else { + assetServer.ServeHTTP(c.Response(), c.Request()) + } + return nil + }) + + if devServerAddr := d.devServerAddr; devServerAddr != "" { + // Start server + go func(server *echo.Echo, log *logger.Logger) { + err := server.Start(devServerAddr) + if err != nil { + log.Error(err.Error()) + } + d.LogDebug("Shutdown completed") + }(d.server, d.logger) + + d.LogDebug("Serving DevServer at http://%s", devServerAddr) + } + + // Launch desktop app + err = d.Frontend.Run(ctx) + + return err +} + +func (d *DevWebServer) WindowReload() { + d.broadcast("reload") + d.Frontend.WindowReload() +} + +func (d *DevWebServer) WindowReloadApp() { + d.broadcast("reloadapp") + d.Frontend.WindowReloadApp() +} + +func (d *DevWebServer) Notify(name string, data ...interface{}) { + d.notify(name, data...) +} + +func (d *DevWebServer) handleReload(c echo.Context) error { + d.WindowReload() + return c.NoContent(http.StatusNoContent) +} + +func (d *DevWebServer) handleReloadApp(c echo.Context) error { + d.WindowReloadApp() + return c.NoContent(http.StatusNoContent) +} + +func (d *DevWebServer) handleIPCWebSocket(c echo.Context) error { + conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + d.logger.Error("WebSocket upgrade failed %v", err) + return err + } + d.LogDebug(fmt.Sprintf("WebSocket client %p connected", conn)) + + d.socketMutex.Lock() + d.websocketClients[conn] = &sync.Mutex{} + locker := d.websocketClients[conn] + d.socketMutex.Unlock() + + var wg sync.WaitGroup + + defer func() { + wg.Wait() + d.socketMutex.Lock() + delete(d.websocketClients, conn) + d.socketMutex.Unlock() + d.LogDebug(fmt.Sprintf("WebSocket client %p disconnected", conn)) + conn.Close() + }() + + for { + _, msgBytes, err := conn.ReadMessage() + if err != nil { + break + } + + msg := string(msgBytes) + wg.Add(1) + + go func(m string) { + defer wg.Done() + + if m == "drag" { + return + } + + if len(m) > 2 && strings.HasPrefix(m, "EE") { + d.notifyExcludingSender([]byte(m), conn) + } + + result, err := d.dispatcher.ProcessMessage(m, d) + if err != nil { + d.logger.Error(err.Error()) + } + + if result != "" { + locker.Lock() + defer locker.Unlock() + if err := conn.WriteMessage(websocket.TextMessage, []byte(result)); err != nil { + d.logger.Error("Websocket write message failed %v", err) + } + } + }(msg) + } + + return nil +} + +func (d *DevWebServer) LogDebug(message string, args ...interface{}) { + d.logger.Debug("[DevWebServer] "+message, args...) +} + +type EventNotify struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (d *DevWebServer) broadcast(message string) { + d.socketMutex.Lock() + defer d.socketMutex.Unlock() + for client, locker := range d.websocketClients { + go func(client *websocket.Conn, locker *sync.Mutex) { + if client == nil { + d.logger.Error("Lost connection to websocket server") + return + } + locker.Lock() + err := client.WriteMessage(websocket.TextMessage, []byte(message)) + if err != nil { + locker.Unlock() + d.logger.Error(err.Error()) + return + } + locker.Unlock() + }(client, locker) + } +} + +func (d *DevWebServer) notify(name string, data ...interface{}) { + // Notify + notification := EventNotify{ + Name: name, + Data: data, + } + payload, err := json.Marshal(notification) + if err != nil { + d.logger.Error(err.Error()) + return + } + d.broadcast("n" + string(payload)) +} + +func (d *DevWebServer) broadcastExcludingSender(message string, sender *websocket.Conn) { + d.socketMutex.Lock() + defer d.socketMutex.Unlock() + for client, locker := range d.websocketClients { + go func(client *websocket.Conn, locker *sync.Mutex) { + if client == sender { + return + } + locker.Lock() + err := client.WriteMessage(websocket.TextMessage, []byte(message)) + if err != nil { + locker.Unlock() + d.logger.Error(err.Error()) + return + } + locker.Unlock() + }(client, locker) + } +} + +func (d *DevWebServer) notifyExcludingSender(eventMessage []byte, sender *websocket.Conn) { + message := "n" + string(eventMessage[2:]) + d.broadcastExcludingSender(message, sender) + + var notifyMessage EventNotify + err := json.Unmarshal(eventMessage[2:], ¬ifyMessage) + if err != nil { + d.logger.Error(err.Error()) + return + } + d.Frontend.Notify(notifyMessage.Name, notifyMessage.Data...) +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher, menuManager *menumanager.Manager, desktopFrontend frontend.Frontend) *DevWebServer { + result := &DevWebServer{ + ctx: ctx, + Frontend: desktopFrontend, + appoptions: appoptions, + logger: myLogger, + appBindings: appBindings, + dispatcher: dispatcher, + server: echo.New(), + menuManager: menuManager, + websocketClients: make(map[*websocket.Conn]*sync.Mutex), + } + + result.devServerAddr, _ = ctx.Value("devserver").(string) + result.server.HideBanner = true + result.server.HidePort = true + return result +} diff --git a/v2/internal/frontend/dispatcher.go b/v2/internal/frontend/dispatcher.go new file mode 100644 index 000000000..367f4cdc8 --- /dev/null +++ b/v2/internal/frontend/dispatcher.go @@ -0,0 +1,5 @@ +package frontend + +type Dispatcher interface { + ProcessMessage(message string, sender Frontend) (string, error) +} diff --git a/v2/internal/frontend/dispatcher/browser.go b/v2/internal/frontend/dispatcher/browser.go new file mode 100644 index 000000000..9ff02e8d3 --- /dev/null +++ b/v2/internal/frontend/dispatcher/browser.go @@ -0,0 +1,23 @@ +package dispatcher + +import ( + "errors" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +// processBrowserMessage processing browser messages +func (d *Dispatcher) processBrowserMessage(message string, sender frontend.Frontend) (string, error) { + if len(message) < 2 { + return "", errors.New("Invalid Browser Message: " + message) + } + switch message[1] { + case 'O': + url := message[3:] + go sender.BrowserOpenURL(url) + default: + d.log.Error("unknown Browser message: %s", message) + } + + return "", nil +} diff --git a/v2/internal/frontend/dispatcher/calls.go b/v2/internal/frontend/dispatcher/calls.go new file mode 100644 index 000000000..ba1062913 --- /dev/null +++ b/v2/internal/frontend/dispatcher/calls.go @@ -0,0 +1,86 @@ +package dispatcher + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type callMessage struct { + Name string `json:"name"` + Args []json.RawMessage `json:"args"` + CallbackID string `json:"callbackID"` +} + +func (d *Dispatcher) processCallMessage(message string, sender frontend.Frontend) (string, error) { + var payload callMessage + err := json.Unmarshal([]byte(message[1:]), &payload) + if err != nil { + return "", err + } + + var result interface{} + + // Handle different calls + switch true { + case strings.HasPrefix(payload.Name, systemCallPrefix): + result, err = d.processSystemCall(payload, sender) + default: + // Lookup method + registeredMethod := d.bindingsDB.GetMethod(payload.Name) + + // Check we have it + if registeredMethod == nil { + return "", fmt.Errorf("method '%s' not registered", payload.Name) + } + + args, err2 := registeredMethod.ParseArgs(payload.Args) + if err2 != nil { + errmsg := fmt.Errorf("error parsing arguments: %s", err2.Error()) + result, _ := d.NewErrorCallback(errmsg.Error(), payload.CallbackID) + return result, errmsg + } + result, err = registeredMethod.Call(args) + } + + callbackMessage := &CallbackMessage{ + CallbackID: payload.CallbackID, + } + if err != nil { + // Use the error formatter if one was provided + if d.errfmt != nil { + callbackMessage.Err = d.errfmt(err) + } else { + callbackMessage.Err = err.Error() + } + } else { + callbackMessage.Result = result + } + messageData, err := json.Marshal(callbackMessage) + d.log.Trace("json call result data: %+v\n", string(messageData)) + if err != nil { + // what now? + d.log.Fatal(err.Error()) + } + + return "c" + string(messageData), nil +} + +// CallbackMessage defines a message that contains the result of a call +type CallbackMessage struct { + Result interface{} `json:"result"` + Err any `json:"error"` + CallbackID string `json:"callbackid"` +} + +func (d *Dispatcher) NewErrorCallback(message string, callbackID string) (string, error) { + result := &CallbackMessage{ + CallbackID: callbackID, + Err: message, + } + messageData, err := json.Marshal(result) + d.log.Trace("json call result data: %+v\n", string(messageData)) + return string(messageData), err +} diff --git a/v2/internal/frontend/dispatcher/dispatcher.go b/v2/internal/frontend/dispatcher/dispatcher.go new file mode 100644 index 000000000..24a43cfef --- /dev/null +++ b/v2/internal/frontend/dispatcher/dispatcher.go @@ -0,0 +1,81 @@ +package dispatcher + +import ( + "context" + "fmt" + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +type Dispatcher struct { + log *logger.Logger + bindings *binding.Bindings + events frontend.Events + bindingsDB *binding.DB + ctx context.Context + errfmt options.ErrorFormatter + disablePanicRecovery bool +} + +func NewDispatcher(ctx context.Context, log *logger.Logger, bindings *binding.Bindings, events frontend.Events, errfmt options.ErrorFormatter, disablePanicRecovery bool) *Dispatcher { + return &Dispatcher{ + log: log, + bindings: bindings, + events: events, + bindingsDB: bindings.DB(), + ctx: ctx, + errfmt: errfmt, + disablePanicRecovery: disablePanicRecovery, + } +} + +func (d *Dispatcher) ProcessMessage(message string, sender frontend.Frontend) (_ string, err error) { + if !d.disablePanicRecovery { + defer func() { + if e := recover(); e != nil { + if errPanic, ok := e.(error); ok { + err = errPanic + } else { + err = fmt.Errorf("%v", e) + } + } + if err != nil { + d.log.Error("process message error: %s -> %s", message, err) + } + }() + } + + if message == "" { + return "", errors.New("No message to process") + } + switch message[0] { + case 'L': + return d.processLogMessage(message) + case 'E': + return d.processEventMessage(message, sender) + case 'C': + return d.processCallMessage(message, sender) + case 'c': + return d.processSecureCallMessage(message, sender) + case 'W': + return d.processWindowMessage(message, sender) + case 'B': + return d.processBrowserMessage(message, sender) + case 'D': + return d.processDragAndDropMessage(message) + case 'Q': + sender.Quit() + return "", nil + case 'S': + sender.Show() + return "", nil + case 'H': + sender.Hide() + return "", nil + default: + return "", errors.New("Unknown message from front end: " + message) + } +} diff --git a/v2/internal/frontend/dispatcher/draganddrop.go b/v2/internal/frontend/dispatcher/draganddrop.go new file mode 100644 index 000000000..8266ec712 --- /dev/null +++ b/v2/internal/frontend/dispatcher/draganddrop.go @@ -0,0 +1,38 @@ +package dispatcher + +import ( + "errors" + "strconv" + "strings" +) + +func (d *Dispatcher) processDragAndDropMessage(message string) (string, error) { + switch message[1] { + case 'D': + msg := strings.SplitN(message[3:], ":", 3) + if len(msg) != 3 { + return "", errors.New("Invalid drag and drop Message: " + message) + } + + x, err := strconv.Atoi(msg[0]) + if err != nil { + return "", errors.New("Invalid x coordinate in drag and drop Message: " + message) + } + + y, err := strconv.Atoi(msg[1]) + if err != nil { + return "", errors.New("Invalid y coordinate in drag and drop Message: " + message) + } + + paths := strings.Split(msg[2], "\n") + if len(paths) < 1 { + return "", errors.New("Invalid drag and drop Message: " + message) + } + + d.events.Emit("wails:file-drop", x, y, paths) + default: + return "", errors.New("Invalid drag and drop Message: " + message) + } + + return "", nil +} diff --git a/v2/internal/frontend/dispatcher/events.go b/v2/internal/frontend/dispatcher/events.go new file mode 100644 index 000000000..12fe7b89e --- /dev/null +++ b/v2/internal/frontend/dispatcher/events.go @@ -0,0 +1,34 @@ +package dispatcher + +import ( + "encoding/json" + "errors" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type EventMessage struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (d *Dispatcher) processEventMessage(message string, sender frontend.Frontend) (string, error) { + if len(message) < 3 { + return "", errors.New("Invalid Event Message: " + message) + } + + switch message[1] { + case 'E': + var eventMessage EventMessage + err := json.Unmarshal([]byte(message[2:]), &eventMessage) + if err != nil { + return "", err + } + go d.events.Notify(sender, eventMessage.Name, eventMessage.Data...) + case 'X': + eventName := message[2:] + go d.events.Off(eventName) + } + + return "", nil +} diff --git a/v2/internal/frontend/dispatcher/log.go b/v2/internal/frontend/dispatcher/log.go new file mode 100644 index 000000000..e42555397 --- /dev/null +++ b/v2/internal/frontend/dispatcher/log.go @@ -0,0 +1,49 @@ +package dispatcher + +import ( + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/logger" + pkgLogger "github.com/wailsapp/wails/v2/pkg/logger" +) + +var logLevelMap = map[byte]logger.LogLevel{ + '1': pkgLogger.TRACE, + '2': pkgLogger.DEBUG, + '3': pkgLogger.INFO, + '4': pkgLogger.WARNING, + '5': pkgLogger.ERROR, +} + +func (d *Dispatcher) processLogMessage(message string) (string, error) { + if len(message) < 3 { + return "", errors.New("Invalid Log Message: " + message) + } + + messageText := message[2:] + + switch message[1] { + case 'T': + d.log.Trace(messageText) + case 'P': + d.log.Print(messageText) + case 'D': + d.log.Debug(messageText) + case 'I': + d.log.Info(messageText) + case 'W': + d.log.Warning(messageText) + case 'E': + d.log.Error(messageText) + case 'F': + d.log.Fatal(messageText) + case 'S': + loglevel, exists := logLevelMap[message[2]] + if !exists { + return "", errors.New("Invalid Set Log Level Message: " + message) + } + d.log.SetLogLevel(loglevel) + default: + return "", errors.New("Invalid Log Message: " + message) + } + return "", nil +} diff --git a/v2/internal/frontend/dispatcher/securecalls.go b/v2/internal/frontend/dispatcher/securecalls.go new file mode 100644 index 000000000..8cdcdfb85 --- /dev/null +++ b/v2/internal/frontend/dispatcher/securecalls.go @@ -0,0 +1,57 @@ +package dispatcher + +import ( + "encoding/json" + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type secureCallMessage struct { + ID int `json:"id"` + Args []json.RawMessage `json:"args"` + CallbackID string `json:"callbackID"` +} + +func (d *Dispatcher) processSecureCallMessage(message string, sender frontend.Frontend) (string, error) { + var payload secureCallMessage + err := json.Unmarshal([]byte(message[1:]), &payload) + if err != nil { + return "", err + } + + var result interface{} + + // Lookup method + registeredMethod := d.bindingsDB.GetObfuscatedMethod(payload.ID) + + // Check we have it + if registeredMethod == nil { + return "", fmt.Errorf("method '%d' not registered", payload.ID) + } + + args, err2 := registeredMethod.ParseArgs(payload.Args) + if err2 != nil { + errmsg := fmt.Errorf("error parsing arguments: %s", err2.Error()) + result, _ := d.NewErrorCallback(errmsg.Error(), payload.CallbackID) + return result, errmsg + } + result, err = registeredMethod.Call(args) + + callbackMessage := &CallbackMessage{ + CallbackID: payload.CallbackID, + } + if err != nil { + callbackMessage.Err = err.Error() + } else { + callbackMessage.Result = result + } + messageData, err := json.Marshal(callbackMessage) + d.log.Trace("json call result data: %+v\n", string(messageData)) + if err != nil { + // what now? + d.log.Fatal(err.Error()) + } + + return "c" + string(messageData), nil +} diff --git a/v2/internal/frontend/dispatcher/systemcalls.go b/v2/internal/frontend/dispatcher/systemcalls.go new file mode 100644 index 000000000..b090a416e --- /dev/null +++ b/v2/internal/frontend/dispatcher/systemcalls.go @@ -0,0 +1,67 @@ +package dispatcher + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/wailsapp/wails/v2/pkg/runtime" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +const systemCallPrefix = ":wails:" + +type position struct { + X int `json:"x"` + Y int `json:"y"` +} + +type size struct { + W int `json:"w"` + H int `json:"h"` +} + +func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Frontend) (interface{}, error) { + // Strip prefix + name := strings.TrimPrefix(payload.Name, systemCallPrefix) + + switch name { + case "WindowGetPos": + x, y := sender.WindowGetPosition() + return &position{x, y}, nil + case "WindowGetSize": + w, h := sender.WindowGetSize() + return &size{w, h}, nil + case "ScreenGetAll": + return sender.ScreenGetAll() + case "WindowIsMaximised": + return sender.WindowIsMaximised(), nil + case "WindowIsMinimised": + return sender.WindowIsMinimised(), nil + case "WindowIsNormal": + return sender.WindowIsNormal(), nil + case "WindowIsFullscreen": + return sender.WindowIsFullscreen(), nil + case "Environment": + return runtime.Environment(d.ctx), nil + case "ClipboardGetText": + t, err := sender.ClipboardGetText() + return t, err + case "ClipboardSetText": + if len(payload.Args) < 1 { + return false, errors.New("empty argument, cannot set clipboard") + } + var arg string + if err := json.Unmarshal(payload.Args[0], &arg); err != nil { + return false, err + } + if err := sender.ClipboardSetText(arg); err != nil { + return false, err + } + return true, nil + default: + return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name) + } +} diff --git a/v2/internal/frontend/dispatcher/window.go b/v2/internal/frontend/dispatcher/window.go new file mode 100644 index 000000000..7e136e069 --- /dev/null +++ b/v2/internal/frontend/dispatcher/window.go @@ -0,0 +1,99 @@ +package dispatcher + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func (d *Dispatcher) mustAtoI(input string) int { + result, err := strconv.Atoi(input) + if err != nil { + d.log.Error("cannot convert %s to integer!", input) + } + return result +} + +func (d *Dispatcher) processWindowMessage(message string, sender frontend.Frontend) (string, error) { + if len(message) < 2 { + return "", errors.New("Invalid Window Message: " + message) + } + + switch message[1] { + case 'A': + switch message[2:] { + case "SDT": + go sender.WindowSetSystemDefaultTheme() + case "LT": + go sender.WindowSetLightTheme() + case "DT": + go sender.WindowSetDarkTheme() + case "TP:0", "TP:1": + if message[2:] == "TP:0" { + go sender.WindowSetAlwaysOnTop(false) + } else if message[2:] == "TP:1" { + go sender.WindowSetAlwaysOnTop(true) + } + } + case 'c': + go sender.WindowCenter() + case 'T': + title := message[2:] + go sender.WindowSetTitle(title) + case 'F': + go sender.WindowFullscreen() + case 'f': + go sender.WindowUnfullscreen() + case 's': + parts := strings.Split(message[3:], ":") + w := d.mustAtoI(parts[0]) + h := d.mustAtoI(parts[1]) + go sender.WindowSetSize(w, h) + case 'p': + parts := strings.Split(message[3:], ":") + x := d.mustAtoI(parts[0]) + y := d.mustAtoI(parts[1]) + go sender.WindowSetPosition(x, y) + case 'H': + go sender.WindowHide() + case 'S': + go sender.WindowShow() + case 'R': + go sender.WindowReloadApp() + case 'r': + var rgba options.RGBA + err := json.Unmarshal([]byte(message[3:]), &rgba) + if err != nil { + return "", err + } + go sender.WindowSetBackgroundColour(&rgba) + case 'M': + go sender.WindowMaximise() + case 't': + go sender.WindowToggleMaximise() + case 'U': + go sender.WindowUnmaximise() + case 'm': + go sender.WindowMinimise() + case 'u': + go sender.WindowUnminimise() + case 'Z': + parts := strings.Split(message[3:], ":") + w := d.mustAtoI(parts[0]) + h := d.mustAtoI(parts[1]) + go sender.WindowSetMaxSize(w, h) + case 'z': + parts := strings.Split(message[3:], ":") + w := d.mustAtoI(parts[0]) + h := d.mustAtoI(parts[1]) + go sender.WindowSetMinSize(w, h) + default: + d.log.Error("unknown Window message: %s", message) + } + + return "", nil +} diff --git a/v2/internal/frontend/events.go b/v2/internal/frontend/events.go new file mode 100644 index 000000000..f690d28a8 --- /dev/null +++ b/v2/internal/frontend/events.go @@ -0,0 +1,11 @@ +package frontend + +type Events interface { + On(eventName string, callback func(...interface{})) func() + OnMultiple(eventName string, callback func(...interface{}), counter int) func() + Once(eventName string, callback func(...interface{})) func() + Emit(eventName string, data ...interface{}) + Off(eventName string) + OffAll() + Notify(sender Frontend, name string, data ...interface{}) +} diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go new file mode 100644 index 000000000..6b2ccbcae --- /dev/null +++ b/v2/internal/frontend/frontend.go @@ -0,0 +1,142 @@ +package frontend + +import ( + "context" + + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" +) + +// FileFilter defines a filter for dialog boxes +type FileFilter struct { + DisplayName string // Filter information EG: "Image Files (*.jpg, *.png)" + Pattern string // semicolon separated list of extensions, EG: "*.jpg;*.png" +} + +// OpenDialogOptions contains the options for the OpenDialogOptions runtime method +type OpenDialogOptions struct { + DefaultDirectory string + DefaultFilename string + Title string + Filters []FileFilter + ShowHiddenFiles bool + CanCreateDirectories bool + ResolvesAliases bool + TreatPackagesAsDirectories bool +} + +// SaveDialogOptions contains the options for the SaveDialog runtime method +type SaveDialogOptions struct { + DefaultDirectory string + DefaultFilename string + Title string + Filters []FileFilter + ShowHiddenFiles bool + CanCreateDirectories bool + TreatPackagesAsDirectories bool +} + +type DialogType string + +const ( + InfoDialog DialogType = "info" + WarningDialog DialogType = "warning" + ErrorDialog DialogType = "error" + QuestionDialog DialogType = "question" +) + +type Screen struct { + IsCurrent bool `json:"isCurrent"` + IsPrimary bool `json:"isPrimary"` + + // Deprecated: Please use Size and PhysicalSize + Width int `json:"width"` + // Deprecated: Please use Size and PhysicalSize + Height int `json:"height"` + + // Size is the size of the screen in logical pixel space, used when setting sizes in Wails + Size ScreenSize `json:"size"` + // PhysicalSize is the physical size of the screen in pixels + PhysicalSize ScreenSize `json:"physicalSize"` +} + +type ScreenSize struct { + Width int `json:"width"` + Height int `json:"height"` +} + +// MessageDialogOptions contains the options for the Message dialogs, EG Info, Warning, etc runtime methods +type MessageDialogOptions struct { + Type DialogType + Title string + Message string + Buttons []string + DefaultButton string + CancelButton string + Icon []byte +} + +type Frontend interface { + Run(ctx context.Context) error + RunMainLoop() + ExecJS(js string) + Hide() + Show() + Quit() + + // Dialog + OpenFileDialog(dialogOptions OpenDialogOptions) (string, error) + OpenMultipleFilesDialog(dialogOptions OpenDialogOptions) ([]string, error) + OpenDirectoryDialog(dialogOptions OpenDialogOptions) (string, error) + SaveFileDialog(dialogOptions SaveDialogOptions) (string, error) + MessageDialog(dialogOptions MessageDialogOptions) (string, error) + + // Window + WindowSetTitle(title string) + WindowShow() + WindowHide() + WindowCenter() + WindowToggleMaximise() + WindowMaximise() + WindowUnmaximise() + WindowMinimise() + WindowUnminimise() + WindowSetAlwaysOnTop(b bool) + WindowSetPosition(x int, y int) + WindowGetPosition() (int, int) + WindowSetSize(width int, height int) + WindowGetSize() (int, int) + WindowSetMinSize(width int, height int) + WindowSetMaxSize(width int, height int) + WindowFullscreen() + WindowUnfullscreen() + WindowSetBackgroundColour(col *options.RGBA) + WindowReload() + WindowReloadApp() + WindowSetSystemDefaultTheme() + WindowSetLightTheme() + WindowSetDarkTheme() + WindowIsMaximised() bool + WindowIsMinimised() bool + WindowIsNormal() bool + WindowIsFullscreen() bool + WindowClose() + WindowPrint() + + // Screen + ScreenGetAll() ([]Screen, error) + + // Menus + MenuSetApplicationMenu(menu *menu.Menu) + MenuUpdateApplicationMenu() + + // Events + Notify(name string, data ...interface{}) + + // Browser + BrowserOpenURL(url string) + + // Clipboard + ClipboardGetText() (string, error) + ClipboardSetText(text string) error +} diff --git a/v2/internal/frontend/originvalidator/originValidator.go b/v2/internal/frontend/originvalidator/originValidator.go new file mode 100644 index 000000000..fd416f945 --- /dev/null +++ b/v2/internal/frontend/originvalidator/originValidator.go @@ -0,0 +1,116 @@ +package originvalidator + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +type OriginValidator struct { + allowedOrigins []string +} + +// NewOriginValidator creates a new validator from a comma-separated string of allowed origins +func NewOriginValidator(startUrl *url.URL, allowedOriginsString string) *OriginValidator { + allowedOrigins := startUrl.Scheme + "://" + startUrl.Host + if allowedOriginsString != "" { + allowedOrigins += "," + allowedOriginsString + } + validator := &OriginValidator{} + validator.parseAllowedOrigins(allowedOrigins) + return validator +} + +// parseAllowedOrigins parses the comma-separated origins string +func (v *OriginValidator) parseAllowedOrigins(originsString string) { + if originsString == "" { + v.allowedOrigins = []string{} + return + } + + origins := strings.Split(originsString, ",") + var trimmedOrigins []string + + for _, origin := range origins { + trimmed := strings.TrimSuffix(strings.TrimSpace(origin), "/") + if trimmed != "" { + trimmedOrigins = append(trimmedOrigins, trimmed) + } + } + + v.allowedOrigins = trimmedOrigins +} + +// IsOriginAllowed checks if the given origin is allowed +func (v *OriginValidator) IsOriginAllowed(origin string) bool { + if origin == "" { + return false + } + + for _, allowedOrigin := range v.allowedOrigins { + if v.matchesOriginPattern(allowedOrigin, origin) { + return true + } + } + + return false +} + +// matchesOriginPattern checks if origin matches the pattern (supports wildcards) +func (v *OriginValidator) matchesOriginPattern(pattern, origin string) bool { + // Exact match + if pattern == origin { + return true + } + + // Wildcard pattern matching + if strings.Contains(pattern, "*") { + regexPattern := v.wildcardPatternToRegex(pattern) + matched, err := regexp.MatchString(regexPattern, origin) + if err != nil { + return false + } + return matched + } + + return false +} + +// wildcardPatternToRegex converts wildcard pattern to regex +func (v *OriginValidator) wildcardPatternToRegex(wildcardPattern string) string { + // Escape special regex characters except * + specialChars := []string{"\\", ".", "+", "?", "^", "$", "{", "}", "(", ")", "|", "[", "]"} + + escaped := wildcardPattern + for _, specialChar := range specialChars { + escaped = strings.ReplaceAll(escaped, specialChar, "\\"+specialChar) + } + + // Replace * with .* (matches any characters) + escaped = strings.ReplaceAll(escaped, "*", ".*") + + // Anchor the pattern to match the entire string + return "^" + escaped + "$" +} + +// GetOriginFromURL extracts origin from URL string +func (v *OriginValidator) GetOriginFromURL(urlString string) (string, error) { + if urlString == "" { + return "", fmt.Errorf("empty URL") + } + + parsedURL, err := url.Parse(urlString) + if err != nil { + return "", fmt.Errorf("invalid URL: %v", err) + } + + if parsedURL.Scheme == "" || parsedURL.Host == "" { + return "", fmt.Errorf("URL missing scheme or host") + } + + // Build origin (scheme + host) + origin := parsedURL.Scheme + "://" + parsedURL.Host + + return origin, nil +} diff --git a/v2/internal/frontend/runtime/assets.go b/v2/internal/frontend/runtime/assets.go new file mode 100644 index 000000000..465452a18 --- /dev/null +++ b/v2/internal/frontend/runtime/assets.go @@ -0,0 +1,26 @@ +//go:build !dev + +package runtime + +var RuntimeAssetsBundle = &RuntimeAssets{ + desktopIPC: DesktopIPC, + runtimeDesktopJS: RuntimeDesktopJS, +} + +type RuntimeAssets struct { + desktopIPC []byte + websocketIPC []byte + runtimeDesktopJS []byte +} + +func (r *RuntimeAssets) DesktopIPC() []byte { + return r.desktopIPC +} + +func (r *RuntimeAssets) WebsocketIPC() []byte { + return r.websocketIPC +} + +func (r *RuntimeAssets) RuntimeDesktopJS() []byte { + return r.runtimeDesktopJS +} diff --git a/v2/internal/frontend/runtime/assets_dev.go b/v2/internal/frontend/runtime/assets_dev.go new file mode 100644 index 000000000..5821403e0 --- /dev/null +++ b/v2/internal/frontend/runtime/assets_dev.go @@ -0,0 +1,27 @@ +//go:build dev + +package runtime + +var RuntimeAssetsBundle = &RuntimeAssets{ + desktopIPC: DesktopIPC, + websocketIPC: WebsocketIPC, + runtimeDesktopJS: RuntimeDesktopJS, +} + +type RuntimeAssets struct { + desktopIPC []byte + websocketIPC []byte + runtimeDesktopJS []byte +} + +func (r *RuntimeAssets) DesktopIPC() []byte { + return r.desktopIPC +} + +func (r *RuntimeAssets) WebsocketIPC() []byte { + return r.websocketIPC +} + +func (r *RuntimeAssets) RuntimeDesktopJS() []byte { + return r.runtimeDesktopJS +} diff --git a/v2/internal/frontend/runtime/desktop/bindings.js b/v2/internal/frontend/runtime/desktop/bindings.js new file mode 100644 index 000000000..96e1890f6 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/bindings.js @@ -0,0 +1,67 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +import {Call} from './calls'; + +// This is where we bind go method wrappers +window.go = {}; + +export function SetBindings(bindingsMap) { + try { + bindingsMap = JSON.parse(bindingsMap); + } catch (e) { + console.error(e); + } + + // Initialise the bindings map + window.go = window.go || {}; + + // Iterate package names + Object.keys(bindingsMap).forEach((packageName) => { + + // Create inner map if it doesn't exist + window.go[packageName] = window.go[packageName] || {}; + + // Iterate struct names + Object.keys(bindingsMap[packageName]).forEach((structName) => { + + // Create inner map if it doesn't exist + window.go[packageName][structName] = window.go[packageName][structName] || {}; + + Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => { + + window.go[packageName][structName][methodName] = function () { + + // No timeout by default + let timeout = 0; + + // Actual function + function dynamic() { + const args = [].slice.call(arguments); + return Call([packageName, structName, methodName].join('.'), args, timeout); + } + + // Allow setting timeout to function + dynamic.setTimeout = function (newTimeout) { + timeout = newTimeout; + }; + + // Allow getting timeout to function + dynamic.getTimeout = function () { + return timeout; + }; + + return dynamic; + }(); + }); + }); + }); +} diff --git a/v2/internal/frontend/runtime/desktop/browser.js b/v2/internal/frontend/runtime/desktop/browser.js new file mode 100644 index 000000000..18c5258f2 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/browser.js @@ -0,0 +1,8 @@ +/** + * @description: Use the system default browser to open the url + * @param {string} url + * @return {void} + */ +export function BrowserOpenURL(url) { + window.WailsInvoke('BO:' + url); +} \ No newline at end of file diff --git a/v2/internal/frontend/runtime/desktop/calls.js b/v2/internal/frontend/runtime/desktop/calls.js new file mode 100644 index 000000000..b41a014b2 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/calls.js @@ -0,0 +1,185 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +export const callbacks = {}; + +/** + * Returns a number from the native browser random function + * + * @returns number + */ +function cryptoRandom() { + var array = new Uint32Array(1); + return window.crypto.getRandomValues(array)[0]; +} + +/** + * Returns a number using da old-skool Math.Random + * I likes to call it LOLRandom + * + * @returns number + */ +function basicRandom() { + return Math.random() * 9007199254740991; +} + +// Pick a random number function based on browser capability +var randomFunc; +if (window.crypto) { + randomFunc = cryptoRandom; +} else { + randomFunc = basicRandom; +} + + +/** + * Call sends a message to the backend to call the binding with the + * given data. A promise is returned and will be completed when the + * backend responds. This will be resolved when the call was successful + * or rejected if an error is passed back. + * There is a timeout mechanism. If the call doesn't respond in the given + * time (in milliseconds) then the promise is rejected. + * + * @export + * @param {string} name + * @param {any=} args + * @param {number=} timeout + * @returns + */ +export function Call(name, args, timeout) { + + // Timeout infinite by default + if (timeout == null) { + timeout = 0; + } + + // Create a promise + return new Promise(function (resolve, reject) { + + // Create a unique callbackID + var callbackID; + do { + callbackID = name + '-' + randomFunc(); + } while (callbacks[callbackID]); + + var timeoutHandle; + // Set timeout + if (timeout > 0) { + timeoutHandle = setTimeout(function () { + reject(Error('Call to ' + name + ' timed out. Request ID: ' + callbackID)); + }, timeout); + } + + // Store callback + callbacks[callbackID] = { + timeoutHandle: timeoutHandle, + reject: reject, + resolve: resolve + }; + + try { + const payload = { + name, + args, + callbackID, + }; + + // Make the call + window.WailsInvoke('C' + JSON.stringify(payload)); + } catch (e) { + // eslint-disable-next-line + console.error(e); + } + }); +} + +window.ObfuscatedCall = (id, args, timeout) => { + + // Timeout infinite by default + if (timeout == null) { + timeout = 0; + } + + // Create a promise + return new Promise(function (resolve, reject) { + + // Create a unique callbackID + var callbackID; + do { + callbackID = id + '-' + randomFunc(); + } while (callbacks[callbackID]); + + var timeoutHandle; + // Set timeout + if (timeout > 0) { + timeoutHandle = setTimeout(function () { + reject(Error('Call to method ' + id + ' timed out. Request ID: ' + callbackID)); + }, timeout); + } + + // Store callback + callbacks[callbackID] = { + timeoutHandle: timeoutHandle, + reject: reject, + resolve: resolve + }; + + try { + const payload = { + id, + args, + callbackID, + }; + + // Make the call + window.WailsInvoke('c' + JSON.stringify(payload)); + } catch (e) { + // eslint-disable-next-line + console.error(e); + } + }); +}; + + +/** + * Called by the backend to return data to a previously called + * binding invocation + * + * @export + * @param {string} incomingMessage + */ +export function Callback(incomingMessage) { + // Parse the message + let message; + try { + message = JSON.parse(incomingMessage); + } catch (e) { + const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`; + runtime.LogDebug(error); + throw new Error(error); + } + let callbackID = message.callbackid; + let callbackData = callbacks[callbackID]; + if (!callbackData) { + const error = `Callback '${callbackID}' not registered!!!`; + console.error(error); // eslint-disable-line + throw new Error(error); + } + clearTimeout(callbackData.timeoutHandle); + + delete callbacks[callbackID]; + + if (message.error) { + callbackData.reject(message.error); + } else { + callbackData.resolve(message.result); + } +} diff --git a/v2/internal/frontend/runtime/desktop/clipboard.js b/v2/internal/frontend/runtime/desktop/clipboard.js new file mode 100644 index 000000000..292cdc32f --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/clipboard.js @@ -0,0 +1,34 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 9 */ + +import {Call} from "./calls"; + +/** + * Set the Size of the window + * + * @export + * @param {string} text + */ +export function ClipboardSetText(text) { + return Call(":wails:ClipboardSetText", [text]); +} + +/** + * Get the text content of the clipboard + * + * @export + * @return {Promise<{string}>} Text content of the clipboard + + */ +export function ClipboardGetText() { + return Call(":wails:ClipboardGetText"); +} \ No newline at end of file diff --git a/v2/internal/frontend/runtime/desktop/contextmenu.js b/v2/internal/frontend/runtime/desktop/contextmenu.js new file mode 100644 index 000000000..b9c397546 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/contextmenu.js @@ -0,0 +1,50 @@ +/* +--default-contextmenu: auto; (default) will show the default context menu if contentEditable is true OR text has been selected OR element is input or textarea +--default-contextmenu: show; will always show the default context menu +--default-contextmenu: hide; will always hide the default context menu + +This rule is inherited like normal CSS rules, so nesting works as expected +*/ +export function processDefaultContextMenu(event) { + // Process default context menu + const element = event.target; + const computedStyle = window.getComputedStyle(element); + const defaultContextMenuAction = computedStyle.getPropertyValue("--default-contextmenu").trim(); + switch (defaultContextMenuAction) { + case "show": + return; + case "hide": + event.preventDefault(); + return; + default: + // Check if contentEditable is true + if (element.isContentEditable) { + return; + } + + // Check if text has been selected and action is on the selected elements + const selection = window.getSelection(); + const hasSelection = (selection.toString().length > 0) + if (hasSelection) { + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const rects = range.getClientRects(); + for (let j = 0; j < rects.length; j++) { + const rect = rects[j]; + if (document.elementFromPoint(rect.left, rect.top) === element) { + return; + } + } + } + } + // Check if tagname is input or textarea + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { + if (hasSelection || (!element.readOnly && !element.disabled)) { + return; + } + } + + // hide default context menu + event.preventDefault(); + } +} diff --git a/v2/internal/frontend/runtime/desktop/draganddrop.js b/v2/internal/frontend/runtime/desktop/draganddrop.js new file mode 100644 index 000000000..e470e4823 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/draganddrop.js @@ -0,0 +1,276 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 9 */ + +import {EventsOn, EventsOff} from "./events"; + +const flags = { + registered: false, + defaultUseDropTarget: true, + useDropTarget: true, + nextDeactivate: null, + nextDeactivateTimeout: null, +}; + +const DROP_TARGET_ACTIVE = "wails-drop-target-active"; + +/** + * checkStyleDropTarget checks if the style has the drop target attribute + * + * @param {CSSStyleDeclaration} style + * @returns + */ +function checkStyleDropTarget(style) { + const cssDropValue = style.getPropertyValue(window.wails.flags.cssDropProperty).trim(); + if (cssDropValue) { + if (cssDropValue === window.wails.flags.cssDropValue) { + return true; + } + // if the element has the drop target attribute, but + // the value is not correct, terminate finding process. + // This can be useful to block some child elements from being drop targets. + return false; + } + return false; +} + +/** + * onDragOver is called when the dragover event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDragOver(e) { + // Check if this is an external file drop or internal HTML drag + // External file drops will have "Files" in the types array + // Internal HTML drags typically have "text/plain", "text/html" or custom types + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (!flags.useDropTarget) { + return; + } + + const element = e.target; + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // if the element is null or element is not child of drop target element + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return; + } + + let currentElement = element; + while (currentElement) { + // check if currentElement is drop target element + if (checkStyleDropTarget(getComputedStyle(currentElement))) { + currentElement.classList.add(DROP_TARGET_ACTIVE); + } + currentElement = currentElement.parentElement; + } +} + +/** + * onDragLeave is called when the dragleave event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDragLeave(e) { + // Check if this is an external file drop or internal HTML drag + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (!flags.useDropTarget) { + return; + } + + // Find the close drop target element + if (!e.target || !checkStyleDropTarget(getComputedStyle(e.target))) { + return null; + } + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // Use debounce technique to tacle dragleave events on overlapping elements and drop target elements + flags.nextDeactivate = () => { + // Deactivate all drop targets, new drop target will be activated on next dragover event + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach(el => el.classList.remove(DROP_TARGET_ACTIVE)); + // Reset nextDeactivate + flags.nextDeactivate = null; + // Clear timeout + if (flags.nextDeactivateTimeout) { + clearTimeout(flags.nextDeactivateTimeout); + flags.nextDeactivateTimeout = null; + } + } + + // Set timeout to deactivate drop targets if not triggered by next drag event + flags.nextDeactivateTimeout = setTimeout(() => { + if(flags.nextDeactivate) flags.nextDeactivate(); + }, 50); +} + +/** + * onDrop is called when the drop event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDrop(e) { + // Check if this is an external file drop or internal HTML drag + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (CanResolveFilePaths()) { + // process files + let files = []; + if (e.dataTransfer.items) { + files = [...e.dataTransfer.items].map((item, i) => { + if (item.kind === 'file') { + return item.getAsFile(); + } + }); + } else { + files = [...e.dataTransfer.files]; + } + window.runtime.ResolveFilePaths(e.x, e.y, files); + } + + if (!flags.useDropTarget) { + return; + } + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // Deactivate all drop targets + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach(el => el.classList.remove(DROP_TARGET_ACTIVE)); +} + +/** + * postMessageWithAdditionalObjects checks the browser's capability of sending postMessageWithAdditionalObjects + * + * @returns {boolean} + * @constructor + */ +export function CanResolveFilePaths() { + return window.chrome?.webview?.postMessageWithAdditionalObjects != null; +} + +/** + * ResolveFilePaths sends drop events to the GO side to resolve file paths on windows. + * + * @param {number} x + * @param {number} y + * @param {any[]} files + * @constructor + */ +export function ResolveFilePaths(x, y, files) { + // Only for windows webview2 >= 1.0.1774.30 + // https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2webmessagereceivedeventargs2?view=webview2-1.0.1823.32#applies-to + if (window.chrome?.webview?.postMessageWithAdditionalObjects) { + chrome.webview.postMessageWithAdditionalObjects(`file:drop:${x}:${y}`, files); + } +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + if (typeof callback !== "function") { + console.error("DragAndDropCallback is not a function"); + return; + } + + if (flags.registered) { + return; + } + flags.registered = true; + + const uDTPT = typeof useDropTarget; + flags.useDropTarget = uDTPT === "undefined" || uDTPT !== "boolean" ? flags.defaultUseDropTarget : useDropTarget; + window.addEventListener('dragover', onDragOver); + window.addEventListener('dragleave', onDragLeave); + window.addEventListener('drop', onDrop); + + let cb = callback; + if (flags.useDropTarget) { + cb = function (x, y, paths) { + const element = document.elementFromPoint(x, y) + // if the element is null or element is not child of drop target element, return null + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return null; + } + callback(x, y, paths); + } + } + + EventsOn("wails:file-drop", cb); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('dragleave', onDragLeave); + window.removeEventListener('drop', onDrop); + EventsOff("wails:file-drop"); + flags.registered = false; +} diff --git a/v2/internal/frontend/runtime/desktop/events.js b/v2/internal/frontend/runtime/desktop/events.js new file mode 100644 index 000000000..e665a8aff --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/events.js @@ -0,0 +1,214 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +// Defines a single listener with a maximum number of times to callback + +/** + * The Listener class defines a listener! :-) + * + * @class Listener + */ +class Listener { + /** + * Creates an instance of Listener. + * @param {string} eventName + * @param {function} callback + * @param {number} maxCallbacks + * @memberof Listener + */ + constructor(eventName, callback, maxCallbacks) { + this.eventName = eventName; + // Default of -1 means infinite + this.maxCallbacks = maxCallbacks || -1; + // Callback invokes the callback with the given data + // Returns true if this listener should be destroyed + this.Callback = (data) => { + callback.apply(null, data); + // If maxCallbacks is infinite, return false (do not destroy) + if (this.maxCallbacks === -1) { + return false; + } + // Decrement maxCallbacks. Return true if now 0, otherwise false + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + }; + } +} + +export const eventListeners = {}; + +/** + * Registers an event listener that will be invoked `maxCallbacks` times before being destroyed + * + * @export + * @param {string} eventName + * @param {function} callback + * @param {number} maxCallbacks + * @returns {function} A function to cancel the listener + */ +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + eventListeners[eventName] = eventListeners[eventName] || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + eventListeners[eventName].push(thisListener); + return () => listenerOff(thisListener); +} + +/** + * Registers an event listener that will be invoked every time the event is emitted + * + * @export + * @param {string} eventName + * @param {function} callback + * @returns {function} A function to cancel the listener + */ +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +/** + * Registers an event listener that will be invoked once then destroyed + * + * @export + * @param {string} eventName + * @param {function} callback + * @returns {function} A function to cancel the listener + */ +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +function notifyListeners(eventData) { + + // Get the event name + let eventName = eventData.name; + + // Keep a list of listener indexes to destroy + const newEventListenerList = eventListeners[eventName]?.slice() || []; + + // Check if we have any listeners for this event + if (newEventListenerList.length) { + + // Iterate listeners + for (let count = newEventListenerList.length - 1; count >= 0; count -= 1) { + + // Get next listener + const listener = newEventListenerList[count]; + + let data = eventData.data; + + // Do the callback + const destroy = listener.Callback(data); + if (destroy) { + // if the listener indicated to destroy itself, add it to the destroy list + newEventListenerList.splice(count, 1); + } + } + + // Update callbacks with new list of listeners + if (newEventListenerList.length === 0) { + removeListener(eventName); + } else { + eventListeners[eventName] = newEventListenerList; + } + } +} + +/** + * Notify informs frontend listeners that an event was emitted with the given data + * + * @export + * @param {string} notifyMessage - encoded notification message + + */ +export function EventsNotify(notifyMessage) { + // Parse the message + let message; + try { + message = JSON.parse(notifyMessage); + } catch (e) { + const error = 'Invalid JSON passed to Notify: ' + notifyMessage; + throw new Error(error); + } + notifyListeners(message); +} + +/** + * Emit an event with the given name and data + * + * @export + * @param {string} eventName + */ +export function EventsEmit(eventName) { + + const payload = { + name: eventName, + data: [].slice.apply(arguments).slice(1), + }; + + // Notify JS listeners + notifyListeners(payload); + + // Notify Go listeners + window.WailsInvoke('EE' + JSON.stringify(payload)); +} + +function removeListener(eventName) { + // Remove local listeners + delete eventListeners[eventName]; + + // Notify Go listeners + window.WailsInvoke('EX' + eventName); +} + +/** + * Off unregisters a listener previously registered with On, + * optionally multiple listeneres can be unregistered via `additionalEventNames` + * + * @param {string} eventName + * @param {...string} additionalEventNames + */ +export function EventsOff(eventName, ...additionalEventNames) { + removeListener(eventName) + + if (additionalEventNames.length > 0) { + additionalEventNames.forEach(eventName => { + removeListener(eventName) + }) + } +} + +/** + * Off unregisters all event listeners previously registered with On + */ + export function EventsOffAll() { + const eventNames = Object.keys(eventListeners); + eventNames.forEach(eventName => { + removeListener(eventName) + }) +} + +/** + * listenerOff unregisters a listener previously registered with EventsOn + * + * @param {Listener} listener + */ + function listenerOff(listener) { + const eventName = listener.eventName; + if (eventListeners[eventName] === undefined) return; + + // Remove local listener + eventListeners[eventName] = eventListeners[eventName].filter(l => l !== listener); + + // Clean up if there are no event listeners left + if (eventListeners[eventName].length === 0) { + removeListener(eventName); + } +} diff --git a/v2/internal/frontend/runtime/desktop/events.test.js b/v2/internal/frontend/runtime/desktop/events.test.js new file mode 100644 index 000000000..69ece676f --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/events.test.js @@ -0,0 +1,132 @@ +import { EventsOnMultiple, EventsNotify, eventListeners, EventsOn, EventsEmit, EventsOffAll, EventsOnce, EventsOff } from './events' +import { expect, describe, it, beforeAll, vi, afterEach, beforeEach } from 'vitest' +// Edit an assertion and save to see HMR in action + +beforeAll(() => { + window.WailsInvoke = vi.fn(() => {}) +}) + +afterEach(() => { + EventsOffAll(); + vi.resetAllMocks() +}) + +describe('EventsOnMultiple', () => { + it('should stop after a specified number of times', () => { + const cb = vi.fn() + EventsOnMultiple('a', cb, 5) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + expect(cb).toBeCalledTimes(5); + expect(window.WailsInvoke).toBeCalledTimes(1); + expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa'); + }) + + it('should return a cancel fn', () => { + const cb = vi.fn() + const cancel = EventsOnMultiple('a', cb, 5) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + cancel() + EventsNotify(JSON.stringify({name: 'a', data: {}})) + EventsNotify(JSON.stringify({name: 'a', data: {}})) + expect(cb).toBeCalledTimes(2) + expect(window.WailsInvoke).toBeCalledTimes(1); + expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa'); + }) +}) + +describe('EventsOn', () => { + it('should create a listener with a count of -1', () => { + EventsOn('a', () => {}) + expect(eventListeners['a'][0].maxCallbacks).toBe(-1) + }) + + it('should return a cancel fn', () => { + const cancel = EventsOn('a', () => {}) + cancel(); + expect(window.WailsInvoke).toBeCalledTimes(1); + expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa'); + }) +}) + +describe('EventsOnce', () => { + it('should create a listener with a count of 1', () => { + EventsOnce('a', () => {}) + expect(eventListeners['a'][0].maxCallbacks).toBe(1) + }) + + it('should return a cancel fn', () => { + const cancel = EventsOn('a', () => {}) + cancel(); + expect(window.WailsInvoke).toBeCalledTimes(1); + expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa'); + }) +}) + +describe('EventsNotify', () => { + it('should inform a listener', () => { + const cb = vi.fn() + EventsOn('a', cb) + EventsNotify(JSON.stringify({name: 'a', data: ["one", "two", "three"]})) + expect(cb).toBeCalledTimes(1); + expect(cb).toHaveBeenLastCalledWith("one", "two", "three"); + expect(window.WailsInvoke).toBeCalledTimes(0); + }) +}) + +describe('EventsEmit', () => { + it('should emit an event', () => { + EventsEmit('a', 'one', 'two', 'three') + expect(window.WailsInvoke).toBeCalledTimes(1); + const calledWith = window.WailsInvoke.calls[0][0]; + expect(calledWith.slice(0, 2)).toBe('EE') + expect(JSON.parse(calledWith.slice(2))).toStrictEqual({data: ["one", "two", "three"], name: "a"}) + }) +}) + +describe('EventsOff', () => { + beforeEach(() => { + EventsOn('a', () => {}) + EventsOn('a', () => {}) + EventsOn('a', () => {}) + EventsOn('b', () => {}) + EventsOn('c', () => {}) + }) + + it('should cancel all event listeners for a single type', () => { + EventsOff('a') + expect(eventListeners['a']).toBeUndefined() + expect(eventListeners['b']).not.toBeUndefined() + expect(eventListeners['c']).not.toBeUndefined() + expect(window.WailsInvoke).toBeCalledTimes(1); + expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa'); + }) + + it('should cancel all event listeners for multiple types', () => { + EventsOff('a', 'b') + expect(eventListeners['a']).toBeUndefined() + expect(eventListeners['b']).toBeUndefined() + expect(eventListeners['c']).not.toBeUndefined() + expect(window.WailsInvoke).toBeCalledTimes(2); + expect(window.WailsInvoke.calls).toStrictEqual([['EXa'], ['EXb']]); + }) +}) + +describe('EventsOffAll', () => { + it('should cancel all event listeners', () => { + EventsOn('a', () => {}) + EventsOn('a', () => {}) + EventsOn('a', () => {}) + EventsOn('b', () => {}) + EventsOn('c', () => {}) + EventsOffAll() + expect(eventListeners).toStrictEqual({}) + expect(window.WailsInvoke).toBeCalledTimes(3); + expect(window.WailsInvoke.calls).toStrictEqual([['EXa'], ['EXb'], ['EXc']]); + }) +}) diff --git a/v2/internal/frontend/runtime/desktop/ipc.js b/v2/internal/frontend/runtime/desktop/ipc.js new file mode 100644 index 000000000..12a23bede --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/ipc.js @@ -0,0 +1,39 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +/** + * WailsInvoke sends the given message to the backend + * + * @param {string} message + */ + +(function () { + // Credit: https://stackoverflow.com/a/2631521 + let _deeptest = function (s) { + var obj = window[s.shift()]; + while (obj && s.length) obj = obj[s.shift()]; + return obj; + }; + let windows = _deeptest(["chrome", "webview", "postMessage"]); + let mac_linux = _deeptest(["webkit", "messageHandlers", "external", "postMessage"]); + + if (!windows && !mac_linux) { + console.error("Unsupported Platform"); + return; + } + + if (windows) { + window.WailsInvoke = (message) => window.chrome.webview.postMessage(message); + } + if (mac_linux) { + window.WailsInvoke = (message) => window.webkit.messageHandlers.external.postMessage(message); + } +})(); \ No newline at end of file diff --git a/v2/internal/frontend/runtime/desktop/log.js b/v2/internal/frontend/runtime/desktop/log.js new file mode 100644 index 000000000..ff52f5919 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/log.js @@ -0,0 +1,113 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 6 */ + +/** + * Sends a log message to the backend with the given level + message + * + * @param {string} level + * @param {string} message + */ +function sendLogMessage(level, message) { + + // Log Message format: + // l[type][message] + window.WailsInvoke('L' + level + message); +} + +/** + * Log the given trace message with the backend + * + * @export + * @param {string} message + */ +export function LogTrace(message) { + sendLogMessage('T', message); +} + +/** + * Log the given message with the backend + * + * @export + * @param {string} message + */ +export function LogPrint(message) { + sendLogMessage('P', message); +} + +/** + * Log the given debug message with the backend + * + * @export + * @param {string} message + */ +export function LogDebug(message) { + sendLogMessage('D', message); +} + +/** + * Log the given info message with the backend + * + * @export + * @param {string} message + */ +export function LogInfo(message) { + sendLogMessage('I', message); +} + +/** + * Log the given warning message with the backend + * + * @export + * @param {string} message + */ +export function LogWarning(message) { + sendLogMessage('W', message); +} + +/** + * Log the given error message with the backend + * + * @export + * @param {string} message + */ +export function LogError(message) { + sendLogMessage('E', message); +} + +/** + * Log the given fatal message with the backend + * + * @export + * @param {string} message + */ +export function LogFatal(message) { + sendLogMessage('F', message); +} + +/** + * Sets the Log level to the given log level + * + * @export + * @param {number} loglevel + */ +export function SetLogLevel(loglevel) { + sendLogMessage('S', loglevel); +} + +// Log levels +export const LogLevel = { + TRACE: 1, + DEBUG: 2, + INFO: 3, + WARNING: 4, + ERROR: 5, +}; diff --git a/v2/internal/frontend/runtime/desktop/main.js b/v2/internal/frontend/runtime/desktop/main.js new file mode 100644 index 000000000..3fda7ef36 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/main.js @@ -0,0 +1,219 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 9 */ +import * as Log from './log'; +import { + eventListeners, + EventsEmit, + EventsNotify, + EventsOff, + EventsOffAll, + EventsOn, + EventsOnce, + EventsOnMultiple, +} from "./events"; +import { Call, Callback, callbacks } from './calls'; +import { SetBindings } from "./bindings"; +import * as Window from "./window"; +import * as Screen from "./screen"; +import * as Browser from "./browser"; +import * as Clipboard from "./clipboard"; +import * as DragAndDrop from "./draganddrop"; +import * as ContextMenu from "./contextmenu"; + +export function Quit() { + window.WailsInvoke('Q'); +} + +export function Show() { + window.WailsInvoke('S'); +} + +export function Hide() { + window.WailsInvoke('H'); +} + +export function Environment() { + return Call(":wails:Environment"); +} + +// The JS runtime +window.runtime = { + ...Log, + ...Window, + ...Browser, + ...Screen, + ...Clipboard, + ...DragAndDrop, + EventsOn, + EventsOnce, + EventsOnMultiple, + EventsEmit, + EventsOff, + EventsOffAll, + Environment, + Show, + Hide, + Quit +}; + +// Internal wails endpoints +window.wails = { + Callback, + EventsNotify, + SetBindings, + eventListeners, + callbacks, + flags: { + disableScrollbarDrag: false, + disableDefaultContextMenu: false, + enableResize: false, + defaultCursor: null, + borderThickness: 6, + shouldDrag: false, + deferDragToMouseMove: true, + cssDragProperty: "--wails-draggable", + cssDragValue: "drag", + cssDropProperty: "--wails-drop-target", + cssDropValue: "drop", + enableWailsDragAndDrop: false, + } +}; + +// Set the bindings +if (window.wailsbindings) { + window.wails.SetBindings(window.wailsbindings); + delete window.wails.SetBindings; +} + +// (bool) This is evaluated at build time in package.json +if (!DEBUG) { + delete window.wailsbindings; +} + +let dragTest = function(e) { + var val = window.getComputedStyle(e.target).getPropertyValue(window.wails.flags.cssDragProperty); + if (val) { + val = val.trim(); + } + + if (val !== window.wails.flags.cssDragValue) { + return false; + } + + if (e.buttons !== 1) { + // Do not start dragging if not the primary button has been clicked. + return false; + } + + if (e.detail !== 1) { + // Do not start dragging if more than once has been clicked, e.g. when double clicking + return false; + } + + return true; +}; + +window.wails.setCSSDragProperties = function(property, value) { + window.wails.flags.cssDragProperty = property; + window.wails.flags.cssDragValue = value; +} + +window.wails.setCSSDropProperties = function(property, value) { + window.wails.flags.cssDropProperty = property; + window.wails.flags.cssDropValue = value; +} + +window.addEventListener('mousedown', (e) => { + // Check for resizing + if (window.wails.flags.resizeEdge) { + window.WailsInvoke("resize:" + window.wails.flags.resizeEdge); + e.preventDefault(); + return; + } + + if (dragTest(e)) { + if (window.wails.flags.disableScrollbarDrag) { + // This checks for clicks on the scroll bar + if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) { + return; + } + } + if (window.wails.flags.deferDragToMouseMove) { + window.wails.flags.shouldDrag = true; + } else { + e.preventDefault() + window.WailsInvoke("drag"); + } + return; + } else { + window.wails.flags.shouldDrag = false; + } +}); + +window.addEventListener('mouseup', () => { + window.wails.flags.shouldDrag = false; +}); + +function setResize(cursor) { + document.documentElement.style.cursor = cursor || window.wails.flags.defaultCursor; + window.wails.flags.resizeEdge = cursor; +} + +window.addEventListener('mousemove', function(e) { + if (window.wails.flags.shouldDrag) { + window.wails.flags.shouldDrag = false; + let mousePressed = e.buttons !== undefined ? e.buttons : e.which; + if (mousePressed > 0) { + window.WailsInvoke("drag"); + return; + } + } + if (!window.wails.flags.enableResize) { + return; + } + if (window.wails.flags.defaultCursor == null) { + window.wails.flags.defaultCursor = document.documentElement.style.cursor; + } + if (window.outerWidth - e.clientX < window.wails.flags.borderThickness && window.outerHeight - e.clientY < window.wails.flags.borderThickness) { + document.documentElement.style.cursor = "se-resize"; + } + let rightBorder = window.outerWidth - e.clientX < window.wails.flags.borderThickness; + let leftBorder = e.clientX < window.wails.flags.borderThickness; + let topBorder = e.clientY < window.wails.flags.borderThickness; + let bottomBorder = window.outerHeight - e.clientY < window.wails.flags.borderThickness; + + // If we aren't on an edge, but were, reset the cursor to default + if (!leftBorder && !rightBorder && !topBorder && !bottomBorder && window.wails.flags.resizeEdge !== undefined) { + setResize(); + } else if (rightBorder && bottomBorder) setResize("se-resize"); + else if (leftBorder && bottomBorder) setResize("sw-resize"); + else if (leftBorder && topBorder) setResize("nw-resize"); + else if (topBorder && rightBorder) setResize("ne-resize"); + else if (leftBorder) setResize("w-resize"); + else if (topBorder) setResize("n-resize"); + else if (bottomBorder) setResize("s-resize"); + else if (rightBorder) setResize("e-resize"); + +}); + +// Setup context menu hook +window.addEventListener('contextmenu', function(e) { + // always show the contextmenu in debug & dev + if (DEBUG) return; + + if (window.wails.flags.disableDefaultContextMenu) { + e.preventDefault(); + } else { + ContextMenu.processDefaultContextMenu(e); + } +}); + +window.WailsInvoke("runtime:ready"); \ No newline at end of file diff --git a/v2/internal/frontend/runtime/desktop/screen.js b/v2/internal/frontend/runtime/desktop/screen.js new file mode 100644 index 000000000..f2afb0506 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/screen.js @@ -0,0 +1,25 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 9 */ + + +import {Call} from "./calls"; + + +/** + * Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. + * @export + * @typedef {import('../wrapper/runtime').Screen} Screen + * @return {Promise<{Screen[]}>} The screens + */ +export function ScreenGetAll() { + return Call(":wails:ScreenGetAll"); +} diff --git a/v2/internal/frontend/runtime/desktop/window.js b/v2/internal/frontend/runtime/desktop/window.js new file mode 100644 index 000000000..f24cdd9c2 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/window.js @@ -0,0 +1,269 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 9 */ + + +import {Call} from "./calls"; + +export function WindowReload() { + window.location.reload(); +} + +export function WindowReloadApp() { + window.WailsInvoke('WR'); +} + +export function WindowSetSystemDefaultTheme() { + window.WailsInvoke('WASDT'); +} + +export function WindowSetLightTheme() { + window.WailsInvoke('WALT'); +} + +export function WindowSetDarkTheme() { + window.WailsInvoke('WADT'); +} + +/** + * Place the window in the center of the screen + * + * @export + */ +export function WindowCenter() { + window.WailsInvoke('Wc'); +} + +/** + * Sets the window title + * + * @param {string} title + * @export + */ +export function WindowSetTitle(title) { + window.WailsInvoke('WT' + title); +} + +/** + * Makes the window go fullscreen + * + * @export + */ +export function WindowFullscreen() { + window.WailsInvoke('WF'); +} + +/** + * Reverts the window from fullscreen + * + * @export + */ +export function WindowUnfullscreen() { + window.WailsInvoke('Wf'); +} + +/** + * Returns the state of the window, i.e. whether the window is in full screen mode or not. + * + * @export + * @return {Promise} The state of the window + */ +export function WindowIsFullscreen() { + return Call(":wails:WindowIsFullscreen"); +} + +/** + * Set the Size of the window + * + * @export + * @param {number} width + * @param {number} height + */ +export function WindowSetSize(width, height) { + window.WailsInvoke('Ws:' + width + ':' + height); +} + +/** + * Get the Size of the window + * + * @export + * @return {Promise<{w: number, h: number}>} The size of the window + + */ +export function WindowGetSize() { + return Call(":wails:WindowGetSize"); +} + +/** + * Set the maximum size of the window + * + * @export + * @param {number} width + * @param {number} height + */ +export function WindowSetMaxSize(width, height) { + window.WailsInvoke('WZ:' + width + ':' + height); +} + +/** + * Set the minimum size of the window + * + * @export + * @param {number} width + * @param {number} height + */ +export function WindowSetMinSize(width, height) { + window.WailsInvoke('Wz:' + width + ':' + height); +} + + + +/** + * Set the window AlwaysOnTop or not on top + * + * @export + */ +export function WindowSetAlwaysOnTop(b) { + + window.WailsInvoke('WATP:' + (b ? '1' : '0')); +} + + + + +/** + * Set the Position of the window + * + * @export + * @param {number} x + * @param {number} y + */ +export function WindowSetPosition(x, y) { + window.WailsInvoke('Wp:' + x + ':' + y); +} + +/** + * Get the Position of the window + * + * @export + * @return {Promise<{x: number, y: number}>} The position of the window + */ +export function WindowGetPosition() { + return Call(":wails:WindowGetPos"); +} + +/** + * Hide the Window + * + * @export + */ +export function WindowHide() { + window.WailsInvoke('WH'); +} + +/** + * Show the Window + * + * @export + */ +export function WindowShow() { + window.WailsInvoke('WS'); +} + +/** + * Maximise the Window + * + * @export + */ +export function WindowMaximise() { + window.WailsInvoke('WM'); +} + +/** + * Toggle the Maximise of the Window + * + * @export + */ +export function WindowToggleMaximise() { + window.WailsInvoke('Wt'); +} + +/** + * Unmaximise the Window + * + * @export + */ +export function WindowUnmaximise() { + window.WailsInvoke('WU'); +} + +/** + * Returns the state of the window, i.e. whether the window is maximised or not. + * + * @export + * @return {Promise} The state of the window + */ +export function WindowIsMaximised() { + return Call(":wails:WindowIsMaximised"); +} + +/** + * Minimise the Window + * + * @export + */ +export function WindowMinimise() { + window.WailsInvoke('Wm'); +} + +/** + * Unminimise the Window + * + * @export + */ +export function WindowUnminimise() { + window.WailsInvoke('Wu'); +} + +/** + * Returns the state of the window, i.e. whether the window is minimised or not. + * + * @export + * @return {Promise} The state of the window + */ +export function WindowIsMinimised() { + return Call(":wails:WindowIsMinimised"); +} + +/** + * Returns the state of the window, i.e. whether the window is normal or not. + * + * @export + * @return {Promise} The state of the window + */ +export function WindowIsNormal() { + return Call(":wails:WindowIsNormal"); +} + +/** + * Sets the background colour of the window + * + * @export + * @param {number} R Red + * @param {number} G Green + * @param {number} B Blue + * @param {number} A Alpha + */ +export function WindowSetBackgroundColour(R, G, B, A) { + let rgba = JSON.stringify({r: R || 0, g: G || 0, b: B || 0, a: A || 255}); + window.WailsInvoke('Wr:' + rgba); +} + diff --git a/v2/internal/frontend/runtime/dev/Overlay.svelte b/v2/internal/frontend/runtime/dev/Overlay.svelte new file mode 100644 index 000000000..dfe02b21b --- /dev/null +++ b/v2/internal/frontend/runtime/dev/Overlay.svelte @@ -0,0 +1,54 @@ + + +{#if $overlayVisible } +
+
+
+
+
+{/if} + + \ No newline at end of file diff --git a/v2/internal/frontend/runtime/dev/build.js b/v2/internal/frontend/runtime/dev/build.js new file mode 100644 index 000000000..6e5e8bb2f --- /dev/null +++ b/v2/internal/frontend/runtime/dev/build.js @@ -0,0 +1,15 @@ +/* jshint esversion: 8 */ +const esbuild = require("esbuild"); +const sveltePlugin = require("esbuild-svelte"); + +esbuild + .build({ + entryPoints: ["main.js"], + bundle: true, + minify: true, + outfile: "../ipc_websocket.js", + plugins: [sveltePlugin({compileOptions: {css: true}})], + logLevel: "info", + sourcemap: "inline", + }) + .catch(() => process.exit(1)); \ No newline at end of file diff --git a/v2/internal/frontend/runtime/dev/log.js b/v2/internal/frontend/runtime/dev/log.js new file mode 100644 index 000000000..e128c97f0 --- /dev/null +++ b/v2/internal/frontend/runtime/dev/log.js @@ -0,0 +1,8 @@ +export function log(message) { + // eslint-disable-next-line + console.log( + '%c wails dev %c ' + message + ' ', + 'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem', + 'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem' + ); +} \ No newline at end of file diff --git a/v2/internal/frontend/runtime/dev/main.js b/v2/internal/frontend/runtime/dev/main.js new file mode 100644 index 000000000..c7f31b0f2 --- /dev/null +++ b/v2/internal/frontend/runtime/dev/main.js @@ -0,0 +1,125 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +import {log} from "./log"; +import Overlay from "./Overlay.svelte"; +import {hideOverlay, showOverlay} from "./store"; + +let components = {}; + +let wailsInvokeInternal = null; +let messageQueue = []; + +window.WailsInvoke = (message) => { + if (!wailsInvokeInternal) { + console.log("Queueing: " + message); + messageQueue.push(message); + return; + } + wailsInvokeInternal(message); +}; + +window.addEventListener('DOMContentLoaded', () => { + components.overlay = new Overlay({ + target: document.body, + anchor: document.querySelector('#wails-spinner'), + }); +}); + +let websocket = null; +let connectTimer; + +window.onbeforeunload = function () { + if (websocket) { + websocket.onclose = function () { + }; + websocket.close(); + websocket = null; + } +}; + +// ...and attempt to connect +connect(); + +function setupIPCBridge() { + wailsInvokeInternal = (message) => { + websocket.send(message); + }; + for (let i = 0; i < messageQueue.length; i++) { + console.log("sending queued message: " + messageQueue[i]); + window.WailsInvoke(messageQueue[i]); + } + messageQueue = []; +} + +// Handles incoming websocket connections +function handleConnect() { + log('Connected to backend'); + hideOverlay(); + setupIPCBridge(); + clearInterval(connectTimer); + websocket.onclose = handleDisconnect; + websocket.onmessage = handleMessage; +} + +// Handles websocket disconnects +function handleDisconnect() { + log('Disconnected from backend'); + websocket = null; + showOverlay(); + connect(); +} + +function _connect() { + if (websocket == null) { + websocket = new WebSocket((window.location.protocol.startsWith("https") ? "wss://" : "ws://") + window.location.host + "/wails/ipc"); + websocket.onopen = handleConnect; + websocket.onerror = function (e) { + e.stopImmediatePropagation(); + e.stopPropagation(); + e.preventDefault(); + websocket = null; + return false; + }; + } +} + +// Try to connect to the backend every .5s +function connect() { + _connect(); + connectTimer = setInterval(_connect, 500); +} + +function handleMessage(message) { + + if (message.data === "reload") { + window.runtime.WindowReload(); + return; + } + if (message.data === "reloadapp") { + window.runtime.WindowReloadApp() + return; + } + + // As a bridge we ignore js and css injections + switch (message.data[0]) { + // Notifications + case 'n': + window.wails.EventsNotify(message.data.slice(1)); + break; + case 'c': + const callbackData = message.data.slice(1); + window.wails.Callback(callbackData); + break; + default: + log('Unknown message: ' + message.data); + } +} diff --git a/v2/internal/frontend/runtime/dev/package-lock.json b/v2/internal/frontend/runtime/dev/package-lock.json new file mode 100644 index 000000000..e1bdd46df --- /dev/null +++ b/v2/internal/frontend/runtime/dev/package-lock.json @@ -0,0 +1,1536 @@ +{ + "name": "dev", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "dev", + "version": "2.0.0", + "license": "ISC", + "devDependencies": { + "esbuild": "^0.12.17", + "esbuild-svelte": "^0.5.6", + "npm-run-all": "^4.1.5", + "svelte": "^3.49.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.21.tgz", + "integrity": "sha512-7hyXbU3g94aREufI/5nls7Xcc+RGQeZWZApm6hoBaFvt2BPtpT4TjFMQ9Tb1jU8XyBGz00ShmiyflCogphMHFQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + } + }, + "node_modules/esbuild-svelte": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.5.6.tgz", + "integrity": "sha512-Bz8nU45FrT6sP/Tf3M2rQUuBGxnDSNSPZNIoYwSNt5H+wjSyo/t+zm94tgnOZsR6GgpDMbNQgo4jGbK0NLvEfw==", + "dev": true, + "dependencies": { + "svelte": "^3.42.6" + }, + "peerDependencies": { + "esbuild": ">=0.9.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", + "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", + "dev": true + }, + "node_modules/string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svelte": { + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.52.0.tgz", + "integrity": "sha512-FxcnEUOAVfr10vDU5dVgJN19IvqeHQCS1zfe8vayTfis9A2t5Fhx+JDe5uv/C3j//bB1umpLJ6quhgs9xyUbCQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + } + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.21.tgz", + "integrity": "sha512-7hyXbU3g94aREufI/5nls7Xcc+RGQeZWZApm6hoBaFvt2BPtpT4TjFMQ9Tb1jU8XyBGz00ShmiyflCogphMHFQ==", + "dev": true + }, + "esbuild-svelte": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.5.6.tgz", + "integrity": "sha512-Bz8nU45FrT6sP/Tf3M2rQUuBGxnDSNSPZNIoYwSNt5H+wjSyo/t+zm94tgnOZsR6GgpDMbNQgo4jGbK0NLvEfw==", + "dev": true, + "requires": { + "svelte": "^3.42.6" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", + "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", + "dev": true + }, + "string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svelte": { + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.52.0.tgz", + "integrity": "sha512-FxcnEUOAVfr10vDU5dVgJN19IvqeHQCS1zfe8vayTfis9A2t5Fhx+JDe5uv/C3j//bB1umpLJ6quhgs9xyUbCQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + } + } +} diff --git a/v2/internal/frontend/runtime/dev/package.json b/v2/internal/frontend/runtime/dev/package.json new file mode 100644 index 000000000..567681f32 --- /dev/null +++ b/v2/internal/frontend/runtime/dev/package.json @@ -0,0 +1,18 @@ +{ + "name": "dev", + "version": "2.0.0", + "description": "Wails JS Dev", + "main": "main.js", + "scripts": { + "build": "run-p build:*", + "build:dev": "node build.js" + }, + "author": "Lea Anthony ", + "license": "ISC", + "devDependencies": { + "esbuild": "^0.12.17", + "esbuild-svelte": "^0.5.6", + "npm-run-all": "^4.1.5", + "svelte": "^3.49.0" + } +} diff --git a/v2/internal/frontend/runtime/dev/store.js b/v2/internal/frontend/runtime/dev/store.js new file mode 100644 index 000000000..fc085570b --- /dev/null +++ b/v2/internal/frontend/runtime/dev/store.js @@ -0,0 +1,12 @@ +import {writable} from 'svelte/store'; + +/** Overlay */ +export const overlayVisible = writable(false); + +export function showOverlay() { + overlayVisible.set(true); +} + +export function hideOverlay() { + overlayVisible.set(false); +} diff --git a/v2/internal/frontend/runtime/events.go b/v2/internal/frontend/runtime/events.go new file mode 100644 index 000000000..1f2e0a6e4 --- /dev/null +++ b/v2/internal/frontend/runtime/events.go @@ -0,0 +1,170 @@ +package runtime + +import ( + "sync" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type Logger interface { + Trace(format string, v ...interface{}) +} + +// eventListener holds a callback function which is invoked when +// the event listened for is emitted. It has a counter which indicates +// how the total number of events it is interested in. A value of zero +// means it does not expire (default). +type eventListener struct { + callback func(...interface{}) // Function to call with emitted event data + counter int // The number of times this callback may be called. -1 = infinite + delete bool // Flag to indicate that this listener should be deleted +} + +// Events handles eventing +type Events struct { + log Logger + frontend []frontend.Frontend + + // Go event listeners + listeners map[string][]*eventListener + notifyLock sync.RWMutex +} + +func (e *Events) Notify(sender frontend.Frontend, name string, data ...interface{}) { + e.notifyBackend(name, data...) + for _, thisFrontend := range e.frontend { + if thisFrontend == sender { + continue + } + thisFrontend.Notify(name, data...) + } +} + +func (e *Events) On(eventName string, callback func(...interface{})) func() { + return e.registerListener(eventName, callback, -1) +} + +func (e *Events) OnMultiple(eventName string, callback func(...interface{}), counter int) func() { + return e.registerListener(eventName, callback, counter) +} + +func (e *Events) Once(eventName string, callback func(...interface{})) func() { + return e.registerListener(eventName, callback, 1) +} + +func (e *Events) Emit(eventName string, data ...interface{}) { + e.notifyBackend(eventName, data...) + for _, thisFrontend := range e.frontend { + thisFrontend.Notify(eventName, data...) + } +} + +func (e *Events) Off(eventName string) { + e.unRegisterListener(eventName) +} + +func (e *Events) OffAll() { + e.notifyLock.Lock() + for eventName := range e.listeners { + delete(e.listeners, eventName) + } + e.notifyLock.Unlock() +} + +// NewEvents creates a new log subsystem +func NewEvents(log Logger) *Events { + result := &Events{ + log: log, + listeners: make(map[string][]*eventListener), + } + return result +} + +// registerListener provides a means of subscribing to events of type "eventName" +func (e *Events) registerListener(eventName string, callback func(...interface{}), counter int) func() { + // Create new eventListener + thisListener := &eventListener{ + callback: callback, + counter: counter, + delete: false, + } + e.notifyLock.Lock() + // Append the new listener to the listeners slice + e.listeners[eventName] = append(e.listeners[eventName], thisListener) + e.notifyLock.Unlock() + return func() { + e.notifyLock.Lock() + defer e.notifyLock.Unlock() + + if _, ok := e.listeners[eventName]; !ok { + return + } + e.listeners[eventName] = lo.Filter(e.listeners[eventName], func(l *eventListener, i int) bool { + return l != thisListener + }) + } +} + +// unRegisterListener provides a means of unsubscribing to events of type "eventName" +func (e *Events) unRegisterListener(eventName string) { + e.notifyLock.Lock() + // Clear the listeners + delete(e.listeners, eventName) + e.notifyLock.Unlock() +} + +// Notify backend for the given event name +func (e *Events) notifyBackend(eventName string, data ...interface{}) { + e.notifyLock.Lock() + defer e.notifyLock.Unlock() + + // Get list of event listeners + listeners := e.listeners[eventName] + if listeners == nil { + e.log.Trace("No listeners for event '%s'", eventName) + return + } + + // We have a dirty flag to indicate that there are items to delete + itemsToDelete := false + + // Callback in goroutine + for _, listener := range listeners { + if listener.counter > 0 { + listener.counter-- + } + go listener.callback(data...) + + if listener.counter == 0 { + listener.delete = true + itemsToDelete = true + } + } + + // Do we have items to delete? + if itemsToDelete { + + // Create a new Listeners slice + var newListeners []*eventListener + + // Iterate over current listeners + for _, listener := range listeners { + // If we aren't deleting the listener, add it to the new list + if !listener.delete { + newListeners = append(newListeners, listener) + } + } + + // Save new listeners or remove entry + if len(newListeners) > 0 { + e.listeners[eventName] = newListeners + } else { + delete(e.listeners, eventName) + } + } +} + +func (e *Events) AddFrontend(appFrontend frontend.Frontend) { + e.frontend = append(e.frontend, appFrontend) +} diff --git a/v2/internal/frontend/runtime/events_test.go b/v2/internal/frontend/runtime/events_test.go new file mode 100644 index 000000000..5795befd0 --- /dev/null +++ b/v2/internal/frontend/runtime/events_test.go @@ -0,0 +1,38 @@ +package runtime_test + +import ( + "fmt" + "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "sync" + "testing" +) +import "github.com/matryer/is" + +type mockLogger struct { + Log string +} + +func (t *mockLogger) Trace(format string, args ...interface{}) { + t.Log = fmt.Sprintf(format, args...) +} + +func Test_EventsOn(t *testing.T) { + i := is.New(t) + l := &mockLogger{} + manager := runtime.NewEvents(l) + + // Test On + eventName := "test" + counter := 0 + var wg sync.WaitGroup + wg.Add(1) + manager.On(eventName, func(args ...interface{}) { + // This is called in a goroutine + counter++ + wg.Done() + }) + manager.Emit(eventName, "test payload") + wg.Wait() + i.Equal(1, counter) + +} diff --git a/v2/internal/frontend/runtime/ipc.go b/v2/internal/frontend/runtime/ipc.go new file mode 100644 index 000000000..8f7631c42 --- /dev/null +++ b/v2/internal/frontend/runtime/ipc.go @@ -0,0 +1,6 @@ +package runtime + +import _ "embed" + +//go:embed ipc.js +var DesktopIPC []byte diff --git a/v2/internal/frontend/runtime/ipc.js b/v2/internal/frontend/runtime/ipc.js new file mode 100644 index 000000000..257d503f4 --- /dev/null +++ b/v2/internal/frontend/runtime/ipc.js @@ -0,0 +1 @@ +(()=>{(function(){let n=function(e){for(var s=window[e.shift()];s&&e.length;)s=s[e.shift()];return s},o=n(["chrome","webview","postMessage"]),t=n(["webkit","messageHandlers","external","postMessage"]);if(!o&&!t){console.error("Unsupported Platform");return}o&&(window.WailsInvoke=e=>window.chrome.webview.postMessage(e)),t&&(window.WailsInvoke=e=>window.webkit.messageHandlers.external.postMessage(e))})();})(); diff --git a/v2/internal/frontend/runtime/ipc_websocket.go b/v2/internal/frontend/runtime/ipc_websocket.go new file mode 100644 index 000000000..f8722ed3f --- /dev/null +++ b/v2/internal/frontend/runtime/ipc_websocket.go @@ -0,0 +1,9 @@ +//go:build dev +// +build dev + +package runtime + +import _ "embed" + +//go:embed ipc_websocket.js +var WebsocketIPC []byte diff --git a/v2/internal/frontend/runtime/ipc_websocket.js b/v2/internal/frontend/runtime/ipc_websocket.js new file mode 100644 index 000000000..1ca048df1 --- /dev/null +++ b/v2/internal/frontend/runtime/ipc_websocket.js @@ -0,0 +1,22 @@ +(()=>{function D(t){console.log("%c wails dev %c "+t+" ","background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem","background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem")}function _(){}var A=t=>t;function N(t){return t()}function it(){return Object.create(null)}function b(t){t.forEach(N)}function w(t){return typeof t=="function"}function L(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function ot(t){return Object.keys(t).length===0}function rt(t,...e){if(t==null)return _;let n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function st(t,e,n){t.$$.on_destroy.push(rt(e,n))}var ct=typeof window!="undefined",Ot=ct?()=>window.performance.now():()=>Date.now(),P=ct?t=>requestAnimationFrame(t):_;var x=new Set;function lt(t){x.forEach(e=>{e.c(t)||(x.delete(e),e.f())}),x.size!==0&&P(lt)}function Dt(t){let e;return x.size===0&&P(lt),{promise:new Promise(n=>{x.add(e={c:t,f:n})}),abort(){x.delete(e)}}}var ut=!1;function At(){ut=!0}function Lt(){ut=!1}function Bt(t,e){t.appendChild(e)}function at(t,e,n){let i=R(t);if(!i.getElementById(e)){let o=B("style");o.id=e,o.textContent=n,ft(i,o)}}function R(t){if(!t)return document;let e=t.getRootNode?t.getRootNode():t.ownerDocument;return e&&e.host?e:t.ownerDocument}function Tt(t){let e=B("style");return ft(R(t),e),e.sheet}function ft(t,e){return Bt(t.head||t,e),e.sheet}function W(t,e,n){t.insertBefore(e,n||null)}function S(t){t.parentNode.removeChild(t)}function B(t){return document.createElement(t)}function Jt(t){return document.createTextNode(t)}function dt(){return Jt("")}function ht(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function zt(t){return Array.from(t.childNodes)}function Ht(t,e,{bubbles:n=!1,cancelable:i=!1}={}){let o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,i,e),o}var T=new Map,J=0;function Gt(t){let e=5381,n=t.length;for(;n--;)e=(e<<5)-e^t.charCodeAt(n);return e>>>0}function qt(t,e){let n={stylesheet:Tt(e),rules:{}};return T.set(t,n),n}function pt(t,e,n,i,o,c,s,l=0){let f=16.666/i,r=`{ +`;for(let g=0;g<=1;g+=f){let F=e+(n-e)*c(g);r+=g*100+`%{${s(F,1-F)}} +`}let y=r+`100% {${s(n,1-n)}} +}`,a=`__svelte_${Gt(y)}_${l}`,u=R(t),{stylesheet:h,rules:p}=T.get(u)||qt(u,t);p[a]||(p[a]=!0,h.insertRule(`@keyframes ${a} ${y}`,h.cssRules.length));let v=t.style.animation||"";return t.style.animation=`${v?`${v}, `:""}${a} ${i}ms linear ${o}ms 1 both`,J+=1,a}function Kt(t,e){let n=(t.style.animation||"").split(", "),i=n.filter(e?c=>c.indexOf(e)<0:c=>c.indexOf("__svelte")===-1),o=n.length-i.length;o&&(t.style.animation=i.join(", "),J-=o,J||Nt())}function Nt(){P(()=>{J||(T.forEach(t=>{let{ownerNode:e}=t.stylesheet;e&&S(e)}),T.clear())})}var V;function C(t){V=t}var k=[];var _t=[],z=[],mt=[],Pt=Promise.resolve(),U=!1;function Rt(){U||(U=!0,Pt.then(yt))}function $(t){z.push(t)}var X=new Set,H=0;function yt(){let t=V;do{for(;H{E=null})),E}function Z(t,e,n){t.dispatchEvent(Ht(`${e?"intro":"outro"}${n}`))}var G=new Set,m;function gt(){m={r:0,c:[],p:m}}function bt(){m.r||b(m.c),m=m.p}function I(t,e){t&&t.i&&(G.delete(t),t.i(e))}function Q(t,e,n,i){if(t&&t.o){if(G.has(t))return;G.add(t),m.c.push(()=>{G.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}var Ut={duration:0};function Y(t,e,n,i){let o=e(t,n),c=i?0:1,s=null,l=null,f=null;function r(){f&&Kt(t,f)}function y(u,h){let p=u.b-c;return h*=Math.abs(p),{a:c,b:u.b,d:p,duration:h,start:u.start,end:u.start+h,group:u.group}}function a(u){let{delay:h=0,duration:p=300,easing:v=A,tick:g=_,css:F}=o||Ut,K={start:Ot()+h,b:u};u||(K.group=m,m.r+=1),s||l?l=K:(F&&(r(),f=pt(t,c,u,p,h,v,F)),u&&g(0,1),s=y(K,p),$(()=>Z(t,u,"start")),Dt(O=>{if(l&&O>l.start&&(s=y(l,p),l=null,Z(t,s.b,"start"),F&&(r(),f=pt(t,c,s.b,s.duration,0,v,o.css))),s){if(O>=s.end)g(c=s.b,1-c),Z(t,s.b,"end"),l||(s.b?r():--s.group.r||b(s.group.c)),s=null;else if(O>=s.start){let jt=O-s.start;c=s.a+s.d*v(jt/s.duration),g(c,1-c)}}return!!(s||l)}))}return{run(u){w(o)?Vt().then(()=>{o=o(),a(u)}):a(u)},end(){r(),s=l=null}}}var le=typeof window!="undefined"?window:typeof globalThis!="undefined"?globalThis:global;var ue=new Set(["allowfullscreen","allowpaymentrequest","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);function Xt(t,e,n,i){let{fragment:o,after_update:c}=t.$$;o&&o.m(e,n),i||$(()=>{let s=t.$$.on_mount.map(N).filter(w);t.$$.on_destroy?t.$$.on_destroy.push(...s):b(s),t.$$.on_mount=[]}),c.forEach($)}function wt(t,e){let n=t.$$;n.fragment!==null&&(b(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Zt(t,e){t.$$.dirty[0]===-1&&(k.push(t),Rt(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{let p=h.length?h[0]:u;return r.ctx&&o(r.ctx[a],r.ctx[a]=p)&&(!r.skip_bound&&r.bound[a]&&r.bound[a](p),y&&Zt(t,a)),u}):[],r.update(),y=!0,b(r.before_update),r.fragment=i?i(r.ctx):!1,e.target){if(e.hydrate){At();let a=zt(e.target);r.fragment&&r.fragment.l(a),a.forEach(S)}else r.fragment&&r.fragment.c();e.intro&&I(t.$$.fragment),Xt(t,e.target,e.anchor,e.customElement),Lt(),yt()}C(f)}var Qt;typeof HTMLElement=="function"&&(Qt=class extends HTMLElement{constructor(){super();this.attachShadow({mode:"open"})}connectedCallback(){let{on_mount:t}=this.$$;this.$$.on_disconnect=t.map(N).filter(w);for(let e in this.$$.slotted)this.appendChild(this.$$.slotted[e])}attributeChangedCallback(t,e,n){this[t]=n}disconnectedCallback(){b(this.$$.on_disconnect)}$destroy(){wt(this,1),this.$destroy=_}$on(t,e){if(!w(e))return _;let n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}$set(t){this.$$set&&!ot(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}});var tt=class{$destroy(){wt(this,1),this.$destroy=_}$on(e,n){if(!w(n))return _;let i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{let o=i.indexOf(n);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!ot(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}};var M=[];function Ft(t,e=_){let n,i=new Set;function o(l){if(L(t,l)&&(t=l,n)){let f=!M.length;for(let r of i)r[1](),M.push(r,t);if(f){for(let r=0;r{i.delete(r),i.size===0&&(n(),n=null)}}return{set:o,update:c,subscribe:s}}var q=Ft(!1);function xt(){q.set(!0)}function $t(){q.set(!1)}function et(t,{delay:e=0,duration:n=400,easing:i=A}={}){let o=+getComputedStyle(t).opacity;return{delay:e,duration:n,easing:i,css:c=>`opacity: ${c*o}`}}function Yt(t){at(t,"svelte-181h7z",`.wails-reconnect-overlay.svelte-181h7z{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter:blur(2px) saturate(0%) contrast(50%) brightness(25%);z-index:999999\r + }.wails-reconnect-overlay-content.svelte-181h7z{position:relative;top:50%;transform:translateY(-50%);margin:0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA7CAMAAAAEsocZAAAC91BMVEUAAACzQ0PjMjLkMjLZLS7XLS+vJCjkMjKlEx6uGyHjMDGiFx7GJyrAISjUKy3mMzPlMjLjMzOsGyDKJirkMjK6HyXmMjLgMDC6IiLcMjLULC3MJyrRKSy+IibmMzPmMjK7ISXlMjLIJimzHSLkMjKtGiHZLC7BIifgMDCpGSDFIivcLy+yHSKoGR+eFBzNKCvlMjKxHSPkMTKxHSLmMjLKJyq5ICXDJCe6ISXdLzDkMjLmMzPFJSm2HyTlMTLhMDGyHSKUEBmhFx24HyTCJCjHJijjMzOiFh7mMjJ6BhDaLDCuGyOKABjnMzPGJinJJiquHCGEChSmGB/pMzOiFh7VKy3OKCu1HiSvHCLjMTLMKCrBIyeICxWxHCLDIyjSKizBIyh+CBO9ISa6ISWDChS9Iie1HyXVLC7FJSrLKCrlMjLiMTGPDhicFRywGyKXFBuhFx1/BxO7IiXkMTGeFBx8BxLkMTGnGR/GJCi4ICWsGyGJDxXSLS2yGiHSKi3CJCfnMzPQKiyECRTKJiq6ISWUERq/Iye0HiPDJCjGJSm6ICaPDxiTEBrdLy+3HyXSKiy0HyOQEBi4ICWhFh1+CBO9IieODhfSKyzWLC2LDhh8BxHKKCq7ISWaFBzkMzPqNDTTLC3EJSiHDBacExyvGyO1HyTPKCy+IieoGSC7ISaVEhrMKCvQKyusGyG0HiKACBPIJSq/JCaABxR5BRLEJCnkMzPJJinEJimPDRZ2BRKqHx/jMjLnMzPgMDHULC3NKSvQKSzsNDTWLS7SKyy3HyTKJyrDJSjbLzDYLC6mGB/GJSnVLC61HiPLKCrHJSm/Iye8Iia6ICWzHSKxHCLaLi/PKSupGR+7ICXpMzPbLi/IJinJJSmsGyGrGiCkFx6PDheJCxaFChXBIyfAIieSDxmBCBPlMjLeLzDdLzC5HySMDRe+ISWvGyGcFBzSKSzPJyvMJyrEJCjDIyefFRyWERriMDHUKiy/ISaZExv0NjbwNTXuNDTrMzMI0c+yAAAAu3RSTlMAA8HR/gwGgAj+MEpGCsC+hGpjQjYnIxgWBfzx7urizMrFqqB1bF83KhsR/fz8+/r5+fXv7unZ1tC+t6mmopqKdW1nYVpVRjUeHhIQBPr59/b28/Hx8ODg3NvUw8O/vKeim5aNioiDgn1vZWNjX1xUU1JPTUVFPT08Mi4qJyIh/Pv7+/n4+Pf39fT08/Du7efn5uXj4uHa19XNwsG/vrq2tbSuramlnpyYkpGNiIZ+enRraGVjVVBKOzghdjzRsAAABJVJREFUWMPtllVQG1EYhTc0ASpoobS0FCulUHd3oUjd3d3d3d3d3d2b7CYhnkBCCHGDEIK7Vh56d0NpOgwkYfLQzvA9ZrLfnPvfc+8uVEst/yheBJup3Nya2MjU6pa/jWLZtxjXpZFtVB4uVNI6m5gIruNkVFebqIb5Ug2ym4TIEM/gtUOGbg613oBzjAzZFrZ+lXu/3TIiMXXS5M6HTvrNHeLpZLEh6suGNW9fzZ9zd/qVi2eOHygqi5cDE5GUrJocONgzyqo0UXNSUlKSEhMztFqtXq9vNxImAmS3g7Y6QlbjdBWVGW36jt4wDGTUXjUsafh5zJWRkdFuZGtWGnCRmg+HasiGMUClTTzW0ZuVgLlGDIPM4Lhi0IrVq+tv2hS21fNrSONQgpM9DsJ4t3fM9PkvJuKj2ZjrZwvILKvaSTgciUSirjt6dOfOpyd169bDb9rMOwF9Hj4OD100gY0YXYb299bjzMrqj9doNByJWlVXFB9DT5dmJuvy+cq83JyuS6ayEYSHulKL8dmFnBkrCeZlHKMrC5XRhXGCZB2Ty1fkleRQaMCFT2DBsEafzRFJu7/2MicbKynPhQUDLiZwMWLJZKNLzoLbJBYVcurSmbmn+rcyJ8vCMgmlmaW6gnwun/+3C96VpAUuET1ZgRR36r2xWlnYSnf3oKABA14uXDDvydxHs6cpTV1p3hlJ2rJCiUjIZCByItXg8sHJijuvT64CuMTABUYvb6NN1Jdp1PH7D7f3bo2eS5KvW4RJr7atWT5w4MBBg9zdBw9+37BS7QIoFS5WnIaj12dr1DEXFgdvr4fh4eFl+u/wz8uf3jjHic8s4DL2Dal0IANyUBeCRCcwOBJV26JsjSpGwHVuSai69jvqD+jr56OgtKy0zAAK5mLTVBKVKL5tNthGAR9JneJQ/bFsHNzy+U7IlCYROxtMpIjR0ceoQVnowracLLpAQWETqV361bPoFo3cEbz2zYLZM7t3HWXcxmiBOgttS1ycWkTXMWh4mGigdug9DFdttqCFgTN6nD0q1XEVSoCxEjyFCi2eNC6Z69MRVIImJ6JQSf5gcFVCuF+aDhCa1F6MJFDaiNBQAh2TMfWBjhmLsAxUjG/fmjs0qjJck8D0GPBcuUuZW1LS/tIsPzqmQt17PvZQknlwnf4tHDBc+7t5VV3QQCkdc+Ur8/hdrz0but0RCumWiYbiKmLJ7EVbRomj4Q7+y5wsaXvfTGFpQcHB7n2WbG4MGdniw2Tm8xl5Yhr7MrSYHQ3uampz10aWyHyuzxvqaW/6W4MjXAUD3QV2aw97ZxhGjxCohYf5TpTHMXU1BbsAuoFnkRygVieIGAbqiF7rrH4rfWpKJouBCtyHJF8ctEyGubBa+C6NsMYEUonJFITHZqWBxXUA12Dv76Tf/PgOBmeNiiLG1pcKo1HAq8jLpY4JU1yWEixVNaOgoRJAKBSZHTZTU+wJOMtUDZvlVITC6FTlksyrEBoPHXpxxbzdaqzigUtVDkJVIOtVQ9UEOR4VGUh/kHWq0edJ6CxnZ+eePXva2bnY/cF/I1RLLf8vvwDANdMSMegxcAAAAABJRU5ErkJggg==);background-repeat:no-repeat;background-position:center\r + }.wails-reconnect-overlay-loadingspinner.svelte-181h7z{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#f00 #eee0 #f00 #eee0;border-radius:50%;animation:svelte-181h7z-loadingspin 1s linear infinite;margin:auto;padding:2.5em\r + }@keyframes svelte-181h7z-loadingspin{100%{transform:rotate(360deg)}}`)}function Mt(t){let e,n,i;return{c(){e=B("div"),e.innerHTML='
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", + "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.3.tgz", + "integrity": "sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz", + "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.12", + "@esbuild/linux-loong64": "0.15.12", + "esbuild-android-64": "0.15.12", + "esbuild-android-arm64": "0.15.12", + "esbuild-darwin-64": "0.15.12", + "esbuild-darwin-arm64": "0.15.12", + "esbuild-freebsd-64": "0.15.12", + "esbuild-freebsd-arm64": "0.15.12", + "esbuild-linux-32": "0.15.12", + "esbuild-linux-64": "0.15.12", + "esbuild-linux-arm": "0.15.12", + "esbuild-linux-arm64": "0.15.12", + "esbuild-linux-mips64le": "0.15.12", + "esbuild-linux-ppc64le": "0.15.12", + "esbuild-linux-riscv64": "0.15.12", + "esbuild-linux-s390x": "0.15.12", + "esbuild-netbsd-64": "0.15.12", + "esbuild-openbsd-64": "0.15.12", + "esbuild-sunos-64": "0.15.12", + "esbuild-windows-32": "0.15.12", + "esbuild-windows-64": "0.15.12", + "esbuild-windows-arm64": "0.15.12" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", + "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", + "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", + "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", + "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", + "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", + "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", + "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", + "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", + "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", + "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", + "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", + "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", + "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", + "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", + "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", + "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", + "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", + "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", + "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", + "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "node_modules/happy-dom": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-7.6.0.tgz", + "integrity": "sha512-QnNsiblZdyVDzW5ts6E7ub79JnabqHJeJgt+1WGNq9fSYqS/r/RzzTVXCZSDl6EVkipdwI48B4bgXAnMZPecIw==", + "dev": true, + "dependencies": { + "css.escape": "^1.5.1", + "he": "^1.2.0", + "node-fetch": "^2.x.x", + "sync-request": "^6.1.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/local-pkg": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz", + "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.2.0.tgz", + "integrity": "sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==", + "dev": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-literal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz", + "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz", + "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "dependencies": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "dependencies": { + "get-port": "^3.1.0" + } + }, + "node_modules/then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "dependencies": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/then-request/node_modules/@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", + "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz", + "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz", + "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", + "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.3.tgz", + "integrity": "sha512-aM0auuPPgMSstWvr851hB74g/LKaKBzSxcG3da7ejfZbx08Y21JpZmbmDYrMTCGhVZKqTGwzcnLMwyfz2WzkhQ==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "strip-literal": "^0.4.2", + "tinybench": "^2.3.0", + "tinypool": "^0.3.0", + "tinyspy": "^1.0.2", + "vite": "^3.0.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz", + "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", + "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "dev": true, + "optional": true + }, + "@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.11.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.3.tgz", + "integrity": "sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz", + "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.12", + "@esbuild/linux-loong64": "0.15.12", + "esbuild-android-64": "0.15.12", + "esbuild-android-arm64": "0.15.12", + "esbuild-darwin-64": "0.15.12", + "esbuild-darwin-arm64": "0.15.12", + "esbuild-freebsd-64": "0.15.12", + "esbuild-freebsd-arm64": "0.15.12", + "esbuild-linux-32": "0.15.12", + "esbuild-linux-64": "0.15.12", + "esbuild-linux-arm": "0.15.12", + "esbuild-linux-arm64": "0.15.12", + "esbuild-linux-mips64le": "0.15.12", + "esbuild-linux-ppc64le": "0.15.12", + "esbuild-linux-riscv64": "0.15.12", + "esbuild-linux-s390x": "0.15.12", + "esbuild-netbsd-64": "0.15.12", + "esbuild-openbsd-64": "0.15.12", + "esbuild-sunos-64": "0.15.12", + "esbuild-windows-32": "0.15.12", + "esbuild-windows-64": "0.15.12", + "esbuild-windows-arm64": "0.15.12" + } + }, + "esbuild-android-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", + "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", + "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", + "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", + "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", + "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", + "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", + "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", + "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", + "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", + "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", + "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", + "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", + "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", + "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", + "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", + "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", + "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", + "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", + "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", + "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "happy-dom": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-7.6.0.tgz", + "integrity": "sha512-QnNsiblZdyVDzW5ts6E7ub79JnabqHJeJgt+1WGNq9fSYqS/r/RzzTVXCZSDl6EVkipdwI48B4bgXAnMZPecIw==", + "dev": true, + "requires": { + "css.escape": "^1.5.1", + "he": "^1.2.0", + "node-fetch": "^2.x.x", + "sync-request": "^6.1.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "requires": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "requires": { + "@types/node": "^10.0.3" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + } + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true + }, + "is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "dev": true + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "local-pkg": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz", + "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==", + "dev": true + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.2.0.tgz", + "integrity": "sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==", + "dev": true, + "requires": { + "asap": "~2.0.6" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-literal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz", + "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==", + "dev": true, + "requires": { + "acorn": "^8.8.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "svelte": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz", + "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==", + "dev": true + }, + "sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "requires": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "requires": { + "get-port": "^3.1.0" + } + }, + "then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "dev": true + } + } + }, + "tinybench": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", + "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "dev": true + }, + "tinypool": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz", + "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==", + "dev": true + }, + "tinyspy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz", + "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==", + "dev": true + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vite": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", + "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", + "dev": true, + "requires": { + "esbuild": "^0.15.9", + "fsevents": "~2.3.2", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + } + }, + "vitest": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.3.tgz", + "integrity": "sha512-aM0auuPPgMSstWvr851hB74g/LKaKBzSxcG3da7ejfZbx08Y21JpZmbmDYrMTCGhVZKqTGwzcnLMwyfz2WzkhQ==", + "dev": true, + "requires": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "strip-literal": "^0.4.2", + "tinybench": "^2.3.0", + "tinypool": "^0.3.0", + "tinyspy": "^1.0.2", + "vite": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + } + } +} diff --git a/v2/internal/frontend/runtime/package.json b/v2/internal/frontend/runtime/package.json new file mode 100644 index 000000000..09ff4d50f --- /dev/null +++ b/v2/internal/frontend/runtime/package.json @@ -0,0 +1,23 @@ +{ + "name": "runtime", + "version": "2.0.0", + "description": "Wails JS Runtime", + "main": "index.js", + "scripts": { + "build": "run-p build:*", + "build:ipc-desktop": "npx esbuild desktop/ipc.js --bundle --minify --outfile=ipc.js", + "build:ipc-dev": "cd dev && npm install && npm run build", + "build:runtime-desktop-prod": "npx esbuild desktop/main.js --bundle --minify --outfile=runtime_prod_desktop.js --define:DEBUG=false", + "build:runtime-desktop-debug": "npx esbuild desktop/main.js --bundle --sourcemap=inline --outfile=runtime_debug_desktop.js --define:DEBUG=true", + "test": "vitest" + }, + "author": "Lea Anthony ", + "license": "ISC", + "devDependencies": { + "esbuild": "^0.15.6", + "happy-dom": "^7.6.0", + "npm-run-all": "^4.1.5", + "svelte": "^3.49.0", + "vitest": "^0.24.3" + } +} diff --git a/v2/internal/frontend/runtime/runtime_debug_desktop.go b/v2/internal/frontend/runtime/runtime_debug_desktop.go new file mode 100644 index 000000000..8dff343c0 --- /dev/null +++ b/v2/internal/frontend/runtime/runtime_debug_desktop.go @@ -0,0 +1,8 @@ +//go:build debug || !production + +package runtime + +import _ "embed" + +//go:embed runtime_debug_desktop.js +var RuntimeDesktopJS []byte diff --git a/v2/internal/frontend/runtime/runtime_debug_desktop.js b/v2/internal/frontend/runtime/runtime_debug_desktop.js new file mode 100644 index 000000000..a5f6068e9 --- /dev/null +++ b/v2/internal/frontend/runtime/runtime_debug_desktop.js @@ -0,0 +1,792 @@ +(() => { + var __defProp = Object.defineProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + + // desktop/log.js + var log_exports = {}; + __export(log_exports, { + LogDebug: () => LogDebug, + LogError: () => LogError, + LogFatal: () => LogFatal, + LogInfo: () => LogInfo, + LogLevel: () => LogLevel, + LogPrint: () => LogPrint, + LogTrace: () => LogTrace, + LogWarning: () => LogWarning, + SetLogLevel: () => SetLogLevel + }); + function sendLogMessage(level, message) { + window.WailsInvoke("L" + level + message); + } + function LogTrace(message) { + sendLogMessage("T", message); + } + function LogPrint(message) { + sendLogMessage("P", message); + } + function LogDebug(message) { + sendLogMessage("D", message); + } + function LogInfo(message) { + sendLogMessage("I", message); + } + function LogWarning(message) { + sendLogMessage("W", message); + } + function LogError(message) { + sendLogMessage("E", message); + } + function LogFatal(message) { + sendLogMessage("F", message); + } + function SetLogLevel(loglevel) { + sendLogMessage("S", loglevel); + } + var LogLevel = { + TRACE: 1, + DEBUG: 2, + INFO: 3, + WARNING: 4, + ERROR: 5 + }; + + // desktop/events.js + var Listener = class { + constructor(eventName, callback, maxCallbacks) { + this.eventName = eventName; + this.maxCallbacks = maxCallbacks || -1; + this.Callback = (data) => { + callback.apply(null, data); + if (this.maxCallbacks === -1) { + return false; + } + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + }; + } + }; + var eventListeners = {}; + function EventsOnMultiple(eventName, callback, maxCallbacks) { + eventListeners[eventName] = eventListeners[eventName] || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + eventListeners[eventName].push(thisListener); + return () => listenerOff(thisListener); + } + function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); + } + function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); + } + function notifyListeners(eventData) { + let eventName = eventData.name; + const newEventListenerList = eventListeners[eventName]?.slice() || []; + if (newEventListenerList.length) { + for (let count = newEventListenerList.length - 1; count >= 0; count -= 1) { + const listener = newEventListenerList[count]; + let data = eventData.data; + const destroy = listener.Callback(data); + if (destroy) { + newEventListenerList.splice(count, 1); + } + } + if (newEventListenerList.length === 0) { + removeListener(eventName); + } else { + eventListeners[eventName] = newEventListenerList; + } + } + } + function EventsNotify(notifyMessage) { + let message; + try { + message = JSON.parse(notifyMessage); + } catch (e) { + const error = "Invalid JSON passed to Notify: " + notifyMessage; + throw new Error(error); + } + notifyListeners(message); + } + function EventsEmit(eventName) { + const payload = { + name: eventName, + data: [].slice.apply(arguments).slice(1) + }; + notifyListeners(payload); + window.WailsInvoke("EE" + JSON.stringify(payload)); + } + function removeListener(eventName) { + delete eventListeners[eventName]; + window.WailsInvoke("EX" + eventName); + } + function EventsOff(eventName, ...additionalEventNames) { + removeListener(eventName); + if (additionalEventNames.length > 0) { + additionalEventNames.forEach((eventName2) => { + removeListener(eventName2); + }); + } + } + function EventsOffAll() { + const eventNames = Object.keys(eventListeners); + eventNames.forEach((eventName) => { + removeListener(eventName); + }); + } + function listenerOff(listener) { + const eventName = listener.eventName; + if (eventListeners[eventName] === void 0) + return; + eventListeners[eventName] = eventListeners[eventName].filter((l) => l !== listener); + if (eventListeners[eventName].length === 0) { + removeListener(eventName); + } + } + + // desktop/calls.js + var callbacks = {}; + function cryptoRandom() { + var array = new Uint32Array(1); + return window.crypto.getRandomValues(array)[0]; + } + function basicRandom() { + return Math.random() * 9007199254740991; + } + var randomFunc; + if (window.crypto) { + randomFunc = cryptoRandom; + } else { + randomFunc = basicRandom; + } + function Call(name, args, timeout) { + if (timeout == null) { + timeout = 0; + } + return new Promise(function(resolve, reject) { + var callbackID; + do { + callbackID = name + "-" + randomFunc(); + } while (callbacks[callbackID]); + var timeoutHandle; + if (timeout > 0) { + timeoutHandle = setTimeout(function() { + reject(Error("Call to " + name + " timed out. Request ID: " + callbackID)); + }, timeout); + } + callbacks[callbackID] = { + timeoutHandle, + reject, + resolve + }; + try { + const payload = { + name, + args, + callbackID + }; + window.WailsInvoke("C" + JSON.stringify(payload)); + } catch (e) { + console.error(e); + } + }); + } + window.ObfuscatedCall = (id, args, timeout) => { + if (timeout == null) { + timeout = 0; + } + return new Promise(function(resolve, reject) { + var callbackID; + do { + callbackID = id + "-" + randomFunc(); + } while (callbacks[callbackID]); + var timeoutHandle; + if (timeout > 0) { + timeoutHandle = setTimeout(function() { + reject(Error("Call to method " + id + " timed out. Request ID: " + callbackID)); + }, timeout); + } + callbacks[callbackID] = { + timeoutHandle, + reject, + resolve + }; + try { + const payload = { + id, + args, + callbackID + }; + window.WailsInvoke("c" + JSON.stringify(payload)); + } catch (e) { + console.error(e); + } + }); + }; + function Callback(incomingMessage) { + let message; + try { + message = JSON.parse(incomingMessage); + } catch (e) { + const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`; + runtime.LogDebug(error); + throw new Error(error); + } + let callbackID = message.callbackid; + let callbackData = callbacks[callbackID]; + if (!callbackData) { + const error = `Callback '${callbackID}' not registered!!!`; + console.error(error); + throw new Error(error); + } + clearTimeout(callbackData.timeoutHandle); + delete callbacks[callbackID]; + if (message.error) { + callbackData.reject(message.error); + } else { + callbackData.resolve(message.result); + } + } + + // desktop/bindings.js + window.go = {}; + function SetBindings(bindingsMap) { + try { + bindingsMap = JSON.parse(bindingsMap); + } catch (e) { + console.error(e); + } + window.go = window.go || {}; + Object.keys(bindingsMap).forEach((packageName) => { + window.go[packageName] = window.go[packageName] || {}; + Object.keys(bindingsMap[packageName]).forEach((structName) => { + window.go[packageName][structName] = window.go[packageName][structName] || {}; + Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => { + window.go[packageName][structName][methodName] = function() { + let timeout = 0; + function dynamic() { + const args = [].slice.call(arguments); + return Call([packageName, structName, methodName].join("."), args, timeout); + } + dynamic.setTimeout = function(newTimeout) { + timeout = newTimeout; + }; + dynamic.getTimeout = function() { + return timeout; + }; + return dynamic; + }(); + }); + }); + }); + } + + // desktop/window.js + var window_exports = {}; + __export(window_exports, { + WindowCenter: () => WindowCenter, + WindowFullscreen: () => WindowFullscreen, + WindowGetPosition: () => WindowGetPosition, + WindowGetSize: () => WindowGetSize, + WindowHide: () => WindowHide, + WindowIsFullscreen: () => WindowIsFullscreen, + WindowIsMaximised: () => WindowIsMaximised, + WindowIsMinimised: () => WindowIsMinimised, + WindowIsNormal: () => WindowIsNormal, + WindowMaximise: () => WindowMaximise, + WindowMinimise: () => WindowMinimise, + WindowReload: () => WindowReload, + WindowReloadApp: () => WindowReloadApp, + WindowSetAlwaysOnTop: () => WindowSetAlwaysOnTop, + WindowSetBackgroundColour: () => WindowSetBackgroundColour, + WindowSetDarkTheme: () => WindowSetDarkTheme, + WindowSetLightTheme: () => WindowSetLightTheme, + WindowSetMaxSize: () => WindowSetMaxSize, + WindowSetMinSize: () => WindowSetMinSize, + WindowSetPosition: () => WindowSetPosition, + WindowSetSize: () => WindowSetSize, + WindowSetSystemDefaultTheme: () => WindowSetSystemDefaultTheme, + WindowSetTitle: () => WindowSetTitle, + WindowShow: () => WindowShow, + WindowToggleMaximise: () => WindowToggleMaximise, + WindowUnfullscreen: () => WindowUnfullscreen, + WindowUnmaximise: () => WindowUnmaximise, + WindowUnminimise: () => WindowUnminimise + }); + function WindowReload() { + window.location.reload(); + } + function WindowReloadApp() { + window.WailsInvoke("WR"); + } + function WindowSetSystemDefaultTheme() { + window.WailsInvoke("WASDT"); + } + function WindowSetLightTheme() { + window.WailsInvoke("WALT"); + } + function WindowSetDarkTheme() { + window.WailsInvoke("WADT"); + } + function WindowCenter() { + window.WailsInvoke("Wc"); + } + function WindowSetTitle(title) { + window.WailsInvoke("WT" + title); + } + function WindowFullscreen() { + window.WailsInvoke("WF"); + } + function WindowUnfullscreen() { + window.WailsInvoke("Wf"); + } + function WindowIsFullscreen() { + return Call(":wails:WindowIsFullscreen"); + } + function WindowSetSize(width, height) { + window.WailsInvoke("Ws:" + width + ":" + height); + } + function WindowGetSize() { + return Call(":wails:WindowGetSize"); + } + function WindowSetMaxSize(width, height) { + window.WailsInvoke("WZ:" + width + ":" + height); + } + function WindowSetMinSize(width, height) { + window.WailsInvoke("Wz:" + width + ":" + height); + } + function WindowSetAlwaysOnTop(b) { + window.WailsInvoke("WATP:" + (b ? "1" : "0")); + } + function WindowSetPosition(x, y) { + window.WailsInvoke("Wp:" + x + ":" + y); + } + function WindowGetPosition() { + return Call(":wails:WindowGetPos"); + } + function WindowHide() { + window.WailsInvoke("WH"); + } + function WindowShow() { + window.WailsInvoke("WS"); + } + function WindowMaximise() { + window.WailsInvoke("WM"); + } + function WindowToggleMaximise() { + window.WailsInvoke("Wt"); + } + function WindowUnmaximise() { + window.WailsInvoke("WU"); + } + function WindowIsMaximised() { + return Call(":wails:WindowIsMaximised"); + } + function WindowMinimise() { + window.WailsInvoke("Wm"); + } + function WindowUnminimise() { + window.WailsInvoke("Wu"); + } + function WindowIsMinimised() { + return Call(":wails:WindowIsMinimised"); + } + function WindowIsNormal() { + return Call(":wails:WindowIsNormal"); + } + function WindowSetBackgroundColour(R, G, B, A) { + let rgba = JSON.stringify({ r: R || 0, g: G || 0, b: B || 0, a: A || 255 }); + window.WailsInvoke("Wr:" + rgba); + } + + // desktop/screen.js + var screen_exports = {}; + __export(screen_exports, { + ScreenGetAll: () => ScreenGetAll + }); + function ScreenGetAll() { + return Call(":wails:ScreenGetAll"); + } + + // desktop/browser.js + var browser_exports = {}; + __export(browser_exports, { + BrowserOpenURL: () => BrowserOpenURL + }); + function BrowserOpenURL(url) { + window.WailsInvoke("BO:" + url); + } + + // desktop/clipboard.js + var clipboard_exports = {}; + __export(clipboard_exports, { + ClipboardGetText: () => ClipboardGetText, + ClipboardSetText: () => ClipboardSetText + }); + function ClipboardSetText(text) { + return Call(":wails:ClipboardSetText", [text]); + } + function ClipboardGetText() { + return Call(":wails:ClipboardGetText"); + } + + // desktop/draganddrop.js + var draganddrop_exports = {}; + __export(draganddrop_exports, { + CanResolveFilePaths: () => CanResolveFilePaths, + OnFileDrop: () => OnFileDrop, + OnFileDropOff: () => OnFileDropOff, + ResolveFilePaths: () => ResolveFilePaths + }); + var flags = { + registered: false, + defaultUseDropTarget: true, + useDropTarget: true, + nextDeactivate: null, + nextDeactivateTimeout: null + }; + var DROP_TARGET_ACTIVE = "wails-drop-target-active"; + function checkStyleDropTarget(style) { + const cssDropValue = style.getPropertyValue(window.wails.flags.cssDropProperty).trim(); + if (cssDropValue) { + if (cssDropValue === window.wails.flags.cssDropValue) { + return true; + } + return false; + } + return false; + } + function onDragOver(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (!flags.useDropTarget) { + return; + } + const element = e.target; + if (flags.nextDeactivate) + flags.nextDeactivate(); + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return; + } + let currentElement = element; + while (currentElement) { + if (checkStyleDropTarget(getComputedStyle(currentElement))) { + currentElement.classList.add(DROP_TARGET_ACTIVE); + } + currentElement = currentElement.parentElement; + } + } + function onDragLeave(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (!flags.useDropTarget) { + return; + } + if (!e.target || !checkStyleDropTarget(getComputedStyle(e.target))) { + return null; + } + if (flags.nextDeactivate) + flags.nextDeactivate(); + flags.nextDeactivate = () => { + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE)); + flags.nextDeactivate = null; + if (flags.nextDeactivateTimeout) { + clearTimeout(flags.nextDeactivateTimeout); + flags.nextDeactivateTimeout = null; + } + }; + flags.nextDeactivateTimeout = setTimeout(() => { + if (flags.nextDeactivate) + flags.nextDeactivate(); + }, 50); + } + function onDrop(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (CanResolveFilePaths()) { + let files = []; + if (e.dataTransfer.items) { + files = [...e.dataTransfer.items].map((item, i) => { + if (item.kind === "file") { + return item.getAsFile(); + } + }); + } else { + files = [...e.dataTransfer.files]; + } + window.runtime.ResolveFilePaths(e.x, e.y, files); + } + if (!flags.useDropTarget) { + return; + } + if (flags.nextDeactivate) + flags.nextDeactivate(); + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE)); + } + function CanResolveFilePaths() { + return window.chrome?.webview?.postMessageWithAdditionalObjects != null; + } + function ResolveFilePaths(x, y, files) { + if (window.chrome?.webview?.postMessageWithAdditionalObjects) { + chrome.webview.postMessageWithAdditionalObjects(`file:drop:${x}:${y}`, files); + } + } + function OnFileDrop(callback, useDropTarget) { + if (typeof callback !== "function") { + console.error("DragAndDropCallback is not a function"); + return; + } + if (flags.registered) { + return; + } + flags.registered = true; + const uDTPT = typeof useDropTarget; + flags.useDropTarget = uDTPT === "undefined" || uDTPT !== "boolean" ? flags.defaultUseDropTarget : useDropTarget; + window.addEventListener("dragover", onDragOver); + window.addEventListener("dragleave", onDragLeave); + window.addEventListener("drop", onDrop); + let cb = callback; + if (flags.useDropTarget) { + cb = function(x, y, paths) { + const element = document.elementFromPoint(x, y); + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return null; + } + callback(x, y, paths); + }; + } + EventsOn("wails:file-drop", cb); + } + function OnFileDropOff() { + window.removeEventListener("dragover", onDragOver); + window.removeEventListener("dragleave", onDragLeave); + window.removeEventListener("drop", onDrop); + EventsOff("wails:file-drop"); + flags.registered = false; + } + + // desktop/contextmenu.js + function processDefaultContextMenu(event) { + const element = event.target; + const computedStyle = window.getComputedStyle(element); + const defaultContextMenuAction = computedStyle.getPropertyValue("--default-contextmenu").trim(); + switch (defaultContextMenuAction) { + case "show": + return; + case "hide": + event.preventDefault(); + return; + default: + if (element.isContentEditable) { + return; + } + const selection = window.getSelection(); + const hasSelection = selection.toString().length > 0; + if (hasSelection) { + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const rects = range.getClientRects(); + for (let j = 0; j < rects.length; j++) { + const rect = rects[j]; + if (document.elementFromPoint(rect.left, rect.top) === element) { + return; + } + } + } + } + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { + if (hasSelection || !element.readOnly && !element.disabled) { + return; + } + } + event.preventDefault(); + } + } + + // desktop/main.js + function Quit() { + window.WailsInvoke("Q"); + } + function Show() { + window.WailsInvoke("S"); + } + function Hide() { + window.WailsInvoke("H"); + } + function Environment() { + return Call(":wails:Environment"); + } + window.runtime = { + ...log_exports, + ...window_exports, + ...browser_exports, + ...screen_exports, + ...clipboard_exports, + ...draganddrop_exports, + EventsOn, + EventsOnce, + EventsOnMultiple, + EventsEmit, + EventsOff, + EventsOffAll, + Environment, + Show, + Hide, + Quit + }; + window.wails = { + Callback, + EventsNotify, + SetBindings, + eventListeners, + callbacks, + flags: { + disableScrollbarDrag: false, + disableDefaultContextMenu: false, + enableResize: false, + defaultCursor: null, + borderThickness: 6, + shouldDrag: false, + deferDragToMouseMove: true, + cssDragProperty: "--wails-draggable", + cssDragValue: "drag", + cssDropProperty: "--wails-drop-target", + cssDropValue: "drop", + enableWailsDragAndDrop: false + } + }; + if (window.wailsbindings) { + window.wails.SetBindings(window.wailsbindings); + delete window.wails.SetBindings; + } + if (false) { + delete window.wailsbindings; + } + var dragTest = function(e) { + var val = window.getComputedStyle(e.target).getPropertyValue(window.wails.flags.cssDragProperty); + if (val) { + val = val.trim(); + } + if (val !== window.wails.flags.cssDragValue) { + return false; + } + if (e.buttons !== 1) { + return false; + } + if (e.detail !== 1) { + return false; + } + return true; + }; + window.wails.setCSSDragProperties = function(property, value) { + window.wails.flags.cssDragProperty = property; + window.wails.flags.cssDragValue = value; + }; + window.wails.setCSSDropProperties = function(property, value) { + window.wails.flags.cssDropProperty = property; + window.wails.flags.cssDropValue = value; + }; + window.addEventListener("mousedown", (e) => { + if (window.wails.flags.resizeEdge) { + window.WailsInvoke("resize:" + window.wails.flags.resizeEdge); + e.preventDefault(); + return; + } + if (dragTest(e)) { + if (window.wails.flags.disableScrollbarDrag) { + if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) { + return; + } + } + if (window.wails.flags.deferDragToMouseMove) { + window.wails.flags.shouldDrag = true; + } else { + e.preventDefault(); + window.WailsInvoke("drag"); + } + return; + } else { + window.wails.flags.shouldDrag = false; + } + }); + window.addEventListener("mouseup", () => { + window.wails.flags.shouldDrag = false; + }); + function setResize(cursor) { + document.documentElement.style.cursor = cursor || window.wails.flags.defaultCursor; + window.wails.flags.resizeEdge = cursor; + } + window.addEventListener("mousemove", function(e) { + if (window.wails.flags.shouldDrag) { + window.wails.flags.shouldDrag = false; + let mousePressed = e.buttons !== void 0 ? e.buttons : e.which; + if (mousePressed > 0) { + window.WailsInvoke("drag"); + return; + } + } + if (!window.wails.flags.enableResize) { + return; + } + if (window.wails.flags.defaultCursor == null) { + window.wails.flags.defaultCursor = document.documentElement.style.cursor; + } + if (window.outerWidth - e.clientX < window.wails.flags.borderThickness && window.outerHeight - e.clientY < window.wails.flags.borderThickness) { + document.documentElement.style.cursor = "se-resize"; + } + let rightBorder = window.outerWidth - e.clientX < window.wails.flags.borderThickness; + let leftBorder = e.clientX < window.wails.flags.borderThickness; + let topBorder = e.clientY < window.wails.flags.borderThickness; + let bottomBorder = window.outerHeight - e.clientY < window.wails.flags.borderThickness; + if (!leftBorder && !rightBorder && !topBorder && !bottomBorder && window.wails.flags.resizeEdge !== void 0) { + setResize(); + } else if (rightBorder && bottomBorder) + setResize("se-resize"); + else if (leftBorder && bottomBorder) + setResize("sw-resize"); + else if (leftBorder && topBorder) + setResize("nw-resize"); + else if (topBorder && rightBorder) + setResize("ne-resize"); + else if (leftBorder) + setResize("w-resize"); + else if (topBorder) + setResize("n-resize"); + else if (bottomBorder) + setResize("s-resize"); + else if (rightBorder) + setResize("e-resize"); + }); + window.addEventListener("contextmenu", function(e) { + if (true) + return; + if (window.wails.flags.disableDefaultContextMenu) { + e.preventDefault(); + } else { + processDefaultContextMenu(e); + } + }); + window.WailsInvoke("runtime:ready"); +})(); +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZGVza3RvcC9sb2cuanMiLCAiZGVza3RvcC9ldmVudHMuanMiLCAiZGVza3RvcC9jYWxscy5qcyIsICJkZXNrdG9wL2JpbmRpbmdzLmpzIiwgImRlc2t0b3Avd2luZG93LmpzIiwgImRlc2t0b3Avc2NyZWVuLmpzIiwgImRlc2t0b3AvYnJvd3Nlci5qcyIsICJkZXNrdG9wL2NsaXBib2FyZC5qcyIsICJkZXNrdG9wL2RyYWdhbmRkcm9wLmpzIiwgImRlc2t0b3AvY29udGV4dG1lbnUuanMiLCAiZGVza3RvcC9tYWluLmpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyIvKlxyXG4gXyAgICAgICBfXyAgICAgIF8gX19cclxufCB8ICAgICAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA2ICovXHJcblxyXG4vKipcclxuICogU2VuZHMgYSBsb2cgbWVzc2FnZSB0byB0aGUgYmFja2VuZCB3aXRoIHRoZSBnaXZlbiBsZXZlbCArIG1lc3NhZ2VcclxuICpcclxuICogQHBhcmFtIHtzdHJpbmd9IGxldmVsXHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5mdW5jdGlvbiBzZW5kTG9nTWVzc2FnZShsZXZlbCwgbWVzc2FnZSkge1xyXG5cclxuXHQvLyBMb2cgTWVzc2FnZSBmb3JtYXQ6XHJcblx0Ly8gbFt0eXBlXVttZXNzYWdlXVxyXG5cdHdpbmRvdy5XYWlsc0ludm9rZSgnTCcgKyBsZXZlbCArIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiB0cmFjZSBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1RyYWNlKG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnVCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1ByaW50KG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnUCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBkZWJ1ZyBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ0RlYnVnKG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnRCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBpbmZvIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nSW5mbyhtZXNzYWdlKSB7XHJcblx0c2VuZExvZ01lc3NhZ2UoJ0knLCBtZXNzYWdlKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIExvZyB0aGUgZ2l2ZW4gd2FybmluZyBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1dhcm5pbmcobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdXJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBMb2cgdGhlIGdpdmVuIGVycm9yIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nRXJyb3IobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdFJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBMb2cgdGhlIGdpdmVuIGZhdGFsIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nRmF0YWwobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdGJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBTZXRzIHRoZSBMb2cgbGV2ZWwgdG8gdGhlIGdpdmVuIGxvZyBsZXZlbFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBsb2dsZXZlbFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFNldExvZ0xldmVsKGxvZ2xldmVsKSB7XHJcblx0c2VuZExvZ01lc3NhZ2UoJ1MnLCBsb2dsZXZlbCk7XHJcbn1cclxuXHJcbi8vIExvZyBsZXZlbHNcclxuZXhwb3J0IGNvbnN0IExvZ0xldmVsID0ge1xyXG5cdFRSQUNFOiAxLFxyXG5cdERFQlVHOiAyLFxyXG5cdElORk86IDMsXHJcblx0V0FSTklORzogNCxcclxuXHRFUlJPUjogNSxcclxufTtcclxuIiwgIi8qXHJcbiBfICAgICAgIF9fICAgICAgXyBfX1xyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuLy8gRGVmaW5lcyBhIHNpbmdsZSBsaXN0ZW5lciB3aXRoIGEgbWF4aW11bSBudW1iZXIgb2YgdGltZXMgdG8gY2FsbGJhY2tcclxuXHJcbi8qKlxyXG4gKiBUaGUgTGlzdGVuZXIgY2xhc3MgZGVmaW5lcyBhIGxpc3RlbmVyISA6LSlcclxuICpcclxuICogQGNsYXNzIExpc3RlbmVyXHJcbiAqL1xyXG5jbGFzcyBMaXN0ZW5lciB7XHJcbiAgICAvKipcclxuICAgICAqIENyZWF0ZXMgYW4gaW5zdGFuY2Ugb2YgTGlzdGVuZXIuXHJcbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAgICAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xyXG4gICAgICogQHBhcmFtIHtudW1iZXJ9IG1heENhbGxiYWNrc1xyXG4gICAgICogQG1lbWJlcm9mIExpc3RlbmVyXHJcbiAgICAgKi9cclxuICAgIGNvbnN0cnVjdG9yKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcykge1xyXG4gICAgICAgIHRoaXMuZXZlbnROYW1lID0gZXZlbnROYW1lO1xyXG4gICAgICAgIC8vIERlZmF1bHQgb2YgLTEgbWVhbnMgaW5maW5pdGVcclxuICAgICAgICB0aGlzLm1heENhbGxiYWNrcyA9IG1heENhbGxiYWNrcyB8fCAtMTtcclxuICAgICAgICAvLyBDYWxsYmFjayBpbnZva2VzIHRoZSBjYWxsYmFjayB3aXRoIHRoZSBnaXZlbiBkYXRhXHJcbiAgICAgICAgLy8gUmV0dXJucyB0cnVlIGlmIHRoaXMgbGlzdGVuZXIgc2hvdWxkIGJlIGRlc3Ryb3llZFxyXG4gICAgICAgIHRoaXMuQ2FsbGJhY2sgPSAoZGF0YSkgPT4ge1xyXG4gICAgICAgICAgICBjYWxsYmFjay5hcHBseShudWxsLCBkYXRhKTtcclxuICAgICAgICAgICAgLy8gSWYgbWF4Q2FsbGJhY2tzIGlzIGluZmluaXRlLCByZXR1cm4gZmFsc2UgKGRvIG5vdCBkZXN0cm95KVxyXG4gICAgICAgICAgICBpZiAodGhpcy5tYXhDYWxsYmFja3MgPT09IC0xKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgLy8gRGVjcmVtZW50IG1heENhbGxiYWNrcy4gUmV0dXJuIHRydWUgaWYgbm93IDAsIG90aGVyd2lzZSBmYWxzZVxyXG4gICAgICAgICAgICB0aGlzLm1heENhbGxiYWNrcyAtPSAxO1xyXG4gICAgICAgICAgICByZXR1cm4gdGhpcy5tYXhDYWxsYmFja3MgPT09IDA7XHJcbiAgICAgICAgfTtcclxuICAgIH1cclxufVxyXG5cclxuZXhwb3J0IGNvbnN0IGV2ZW50TGlzdGVuZXJzID0ge307XHJcblxyXG4vKipcclxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGBtYXhDYWxsYmFja3NgIHRpbWVzIGJlZm9yZSBiZWluZyBkZXN0cm95ZWRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAqIEBwYXJhbSB7ZnVuY3Rpb259IGNhbGxiYWNrXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBtYXhDYWxsYmFja3NcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcykge1xyXG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gfHwgW107XHJcbiAgICBjb25zdCB0aGlzTGlzdGVuZXIgPSBuZXcgTGlzdGVuZXIoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKTtcclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0ucHVzaCh0aGlzTGlzdGVuZXIpO1xyXG4gICAgcmV0dXJuICgpID0+IGxpc3RlbmVyT2ZmKHRoaXNMaXN0ZW5lcik7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZWdpc3RlcnMgYW4gZXZlbnQgbGlzdGVuZXIgdGhhdCB3aWxsIGJlIGludm9rZWQgZXZlcnkgdGltZSB0aGUgZXZlbnQgaXMgZW1pdHRlZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbihldmVudE5hbWUsIGNhbGxiYWNrKSB7XHJcbiAgICByZXR1cm4gRXZlbnRzT25NdWx0aXBsZShldmVudE5hbWUsIGNhbGxiYWNrLCAtMSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZWdpc3RlcnMgYW4gZXZlbnQgbGlzdGVuZXIgdGhhdCB3aWxsIGJlIGludm9rZWQgb25jZSB0aGVuIGRlc3Ryb3llZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbmNlKGV2ZW50TmFtZSwgY2FsbGJhY2spIHtcclxuICAgIHJldHVybiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIDEpO1xyXG59XHJcblxyXG5mdW5jdGlvbiBub3RpZnlMaXN0ZW5lcnMoZXZlbnREYXRhKSB7XHJcblxyXG4gICAgLy8gR2V0IHRoZSBldmVudCBuYW1lXHJcbiAgICBsZXQgZXZlbnROYW1lID0gZXZlbnREYXRhLm5hbWU7XHJcblxyXG4gICAgLy8gS2VlcCBhIGxpc3Qgb2YgbGlzdGVuZXIgaW5kZXhlcyB0byBkZXN0cm95XHJcbiAgICBjb25zdCBuZXdFdmVudExpc3RlbmVyTGlzdCA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0/LnNsaWNlKCkgfHwgW107XHJcblxyXG4gICAgLy8gQ2hlY2sgaWYgd2UgaGF2ZSBhbnkgbGlzdGVuZXJzIGZvciB0aGlzIGV2ZW50XHJcbiAgICBpZiAobmV3RXZlbnRMaXN0ZW5lckxpc3QubGVuZ3RoKSB7XHJcblxyXG4gICAgICAgIC8vIEl0ZXJhdGUgbGlzdGVuZXJzXHJcbiAgICAgICAgZm9yIChsZXQgY291bnQgPSBuZXdFdmVudExpc3RlbmVyTGlzdC5sZW5ndGggLSAxOyBjb3VudCA+PSAwOyBjb3VudCAtPSAxKSB7XHJcblxyXG4gICAgICAgICAgICAvLyBHZXQgbmV4dCBsaXN0ZW5lclxyXG4gICAgICAgICAgICBjb25zdCBsaXN0ZW5lciA9IG5ld0V2ZW50TGlzdGVuZXJMaXN0W2NvdW50XTtcclxuXHJcbiAgICAgICAgICAgIGxldCBkYXRhID0gZXZlbnREYXRhLmRhdGE7XHJcblxyXG4gICAgICAgICAgICAvLyBEbyB0aGUgY2FsbGJhY2tcclxuICAgICAgICAgICAgY29uc3QgZGVzdHJveSA9IGxpc3RlbmVyLkNhbGxiYWNrKGRhdGEpO1xyXG4gICAgICAgICAgICBpZiAoZGVzdHJveSkge1xyXG4gICAgICAgICAgICAgICAgLy8gaWYgdGhlIGxpc3RlbmVyIGluZGljYXRlZCB0byBkZXN0cm95IGl0c2VsZiwgYWRkIGl0IHRvIHRoZSBkZXN0cm95IGxpc3RcclxuICAgICAgICAgICAgICAgIG5ld0V2ZW50TGlzdGVuZXJMaXN0LnNwbGljZShjb3VudCwgMSk7XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIC8vIFVwZGF0ZSBjYWxsYmFja3Mgd2l0aCBuZXcgbGlzdCBvZiBsaXN0ZW5lcnNcclxuICAgICAgICBpZiAobmV3RXZlbnRMaXN0ZW5lckxpc3QubGVuZ3RoID09PSAwKSB7XHJcbiAgICAgICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSk7XHJcbiAgICAgICAgfSBlbHNlIHtcclxuICAgICAgICAgICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IG5ld0V2ZW50TGlzdGVuZXJMaXN0O1xyXG4gICAgICAgIH1cclxuICAgIH1cclxufVxyXG5cclxuLyoqXHJcbiAqIE5vdGlmeSBpbmZvcm1zIGZyb250ZW5kIGxpc3RlbmVycyB0aGF0IGFuIGV2ZW50IHdhcyBlbWl0dGVkIHdpdGggdGhlIGdpdmVuIGRhdGFcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbm90aWZ5TWVzc2FnZSAtIGVuY29kZWQgbm90aWZpY2F0aW9uIG1lc3NhZ2VcclxuXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gRXZlbnRzTm90aWZ5KG5vdGlmeU1lc3NhZ2UpIHtcclxuICAgIC8vIFBhcnNlIHRoZSBtZXNzYWdlXHJcbiAgICBsZXQgbWVzc2FnZTtcclxuICAgIHRyeSB7XHJcbiAgICAgICAgbWVzc2FnZSA9IEpTT04ucGFyc2Uobm90aWZ5TWVzc2FnZSk7XHJcbiAgICB9IGNhdGNoIChlKSB7XHJcbiAgICAgICAgY29uc3QgZXJyb3IgPSAnSW52YWxpZCBKU09OIHBhc3NlZCB0byBOb3RpZnk6ICcgKyBub3RpZnlNZXNzYWdlO1xyXG4gICAgICAgIHRocm93IG5ldyBFcnJvcihlcnJvcik7XHJcbiAgICB9XHJcbiAgICBub3RpZnlMaXN0ZW5lcnMobWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBFbWl0IGFuIGV2ZW50IHdpdGggdGhlIGdpdmVuIG5hbWUgYW5kIGRhdGFcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gRXZlbnRzRW1pdChldmVudE5hbWUpIHtcclxuXHJcbiAgICBjb25zdCBwYXlsb2FkID0ge1xyXG4gICAgICAgIG5hbWU6IGV2ZW50TmFtZSxcclxuICAgICAgICBkYXRhOiBbXS5zbGljZS5hcHBseShhcmd1bWVudHMpLnNsaWNlKDEpLFxyXG4gICAgfTtcclxuXHJcbiAgICAvLyBOb3RpZnkgSlMgbGlzdGVuZXJzXHJcbiAgICBub3RpZnlMaXN0ZW5lcnMocGF5bG9hZCk7XHJcblxyXG4gICAgLy8gTm90aWZ5IEdvIGxpc3RlbmVyc1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFRScgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XHJcbn1cclxuXHJcbmZ1bmN0aW9uIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSkge1xyXG4gICAgLy8gUmVtb3ZlIGxvY2FsIGxpc3RlbmVyc1xyXG4gICAgZGVsZXRlIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV07XHJcblxyXG4gICAgLy8gTm90aWZ5IEdvIGxpc3RlbmVyc1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFWCcgKyBldmVudE5hbWUpO1xyXG59XHJcblxyXG4vKipcclxuICogT2ZmIHVucmVnaXN0ZXJzIGEgbGlzdGVuZXIgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT24sXHJcbiAqIG9wdGlvbmFsbHkgbXVsdGlwbGUgbGlzdGVuZXJlcyBjYW4gYmUgdW5yZWdpc3RlcmVkIHZpYSBgYWRkaXRpb25hbEV2ZW50TmFtZXNgXHJcbiAqXHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtICB7Li4uc3RyaW5nfSBhZGRpdGlvbmFsRXZlbnROYW1lc1xyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZihldmVudE5hbWUsIC4uLmFkZGl0aW9uYWxFdmVudE5hbWVzKSB7XHJcbiAgICByZW1vdmVMaXN0ZW5lcihldmVudE5hbWUpXHJcblxyXG4gICAgaWYgKGFkZGl0aW9uYWxFdmVudE5hbWVzLmxlbmd0aCA+IDApIHtcclxuICAgICAgICBhZGRpdGlvbmFsRXZlbnROYW1lcy5mb3JFYWNoKGV2ZW50TmFtZSA9PiB7XHJcbiAgICAgICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSlcclxuICAgICAgICB9KVxyXG4gICAgfVxyXG59XHJcblxyXG4vKipcclxuICogT2ZmIHVucmVnaXN0ZXJzIGFsbCBldmVudCBsaXN0ZW5lcnMgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT25cclxuICovXHJcbiBleHBvcnQgZnVuY3Rpb24gRXZlbnRzT2ZmQWxsKCkge1xyXG4gICAgY29uc3QgZXZlbnROYW1lcyA9IE9iamVjdC5rZXlzKGV2ZW50TGlzdGVuZXJzKTtcclxuICAgIGV2ZW50TmFtZXMuZm9yRWFjaChldmVudE5hbWUgPT4ge1xyXG4gICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSlcclxuICAgIH0pXHJcbn1cclxuXHJcbi8qKlxyXG4gKiBsaXN0ZW5lck9mZiB1bnJlZ2lzdGVycyBhIGxpc3RlbmVyIHByZXZpb3VzbHkgcmVnaXN0ZXJlZCB3aXRoIEV2ZW50c09uXHJcbiAqXHJcbiAqIEBwYXJhbSB7TGlzdGVuZXJ9IGxpc3RlbmVyXHJcbiAqL1xyXG4gZnVuY3Rpb24gbGlzdGVuZXJPZmYobGlzdGVuZXIpIHtcclxuICAgIGNvbnN0IGV2ZW50TmFtZSA9IGxpc3RlbmVyLmV2ZW50TmFtZTtcclxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdID09PSB1bmRlZmluZWQpIHJldHVybjtcclxuXHJcbiAgICAvLyBSZW1vdmUgbG9jYWwgbGlzdGVuZXJcclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmZpbHRlcihsID0+IGwgIT09IGxpc3RlbmVyKTtcclxuXHJcbiAgICAvLyBDbGVhbiB1cCBpZiB0aGVyZSBhcmUgbm8gZXZlbnQgbGlzdGVuZXJzIGxlZnRcclxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmxlbmd0aCA9PT0gMCkge1xyXG4gICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSk7XHJcbiAgICB9XHJcbn1cclxuIiwgIi8qXHJcbiBfICAgICAgIF9fICAgICAgXyBfX1xyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuZXhwb3J0IGNvbnN0IGNhbGxiYWNrcyA9IHt9O1xyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgYSBudW1iZXIgZnJvbSB0aGUgbmF0aXZlIGJyb3dzZXIgcmFuZG9tIGZ1bmN0aW9uXHJcbiAqXHJcbiAqIEByZXR1cm5zIG51bWJlclxyXG4gKi9cclxuZnVuY3Rpb24gY3J5cHRvUmFuZG9tKCkge1xyXG5cdHZhciBhcnJheSA9IG5ldyBVaW50MzJBcnJheSgxKTtcclxuXHRyZXR1cm4gd2luZG93LmNyeXB0by5nZXRSYW5kb21WYWx1ZXMoYXJyYXkpWzBdO1xyXG59XHJcblxyXG4vKipcclxuICogUmV0dXJucyBhIG51bWJlciB1c2luZyBkYSBvbGQtc2tvb2wgTWF0aC5SYW5kb21cclxuICogSSBsaWtlcyB0byBjYWxsIGl0IExPTFJhbmRvbVxyXG4gKlxyXG4gKiBAcmV0dXJucyBudW1iZXJcclxuICovXHJcbmZ1bmN0aW9uIGJhc2ljUmFuZG9tKCkge1xyXG5cdHJldHVybiBNYXRoLnJhbmRvbSgpICogOTAwNzE5OTI1NDc0MDk5MTtcclxufVxyXG5cclxuLy8gUGljayBhIHJhbmRvbSBudW1iZXIgZnVuY3Rpb24gYmFzZWQgb24gYnJvd3NlciBjYXBhYmlsaXR5XHJcbnZhciByYW5kb21GdW5jO1xyXG5pZiAod2luZG93LmNyeXB0bykge1xyXG5cdHJhbmRvbUZ1bmMgPSBjcnlwdG9SYW5kb207XHJcbn0gZWxzZSB7XHJcblx0cmFuZG9tRnVuYyA9IGJhc2ljUmFuZG9tO1xyXG59XHJcblxyXG5cclxuLyoqXHJcbiAqIENhbGwgc2VuZHMgYSBtZXNzYWdlIHRvIHRoZSBiYWNrZW5kIHRvIGNhbGwgdGhlIGJpbmRpbmcgd2l0aCB0aGVcclxuICogZ2l2ZW4gZGF0YS4gQSBwcm9taXNlIGlzIHJldHVybmVkIGFuZCB3aWxsIGJlIGNvbXBsZXRlZCB3aGVuIHRoZVxyXG4gKiBiYWNrZW5kIHJlc3BvbmRzLiBUaGlzIHdpbGwgYmUgcmVzb2x2ZWQgd2hlbiB0aGUgY2FsbCB3YXMgc3VjY2Vzc2Z1bFxyXG4gKiBvciByZWplY3RlZCBpZiBhbiBlcnJvciBpcyBwYXNzZWQgYmFjay5cclxuICogVGhlcmUgaXMgYSB0aW1lb3V0IG1lY2hhbmlzbS4gSWYgdGhlIGNhbGwgZG9lc24ndCByZXNwb25kIGluIHRoZSBnaXZlblxyXG4gKiB0aW1lIChpbiBtaWxsaXNlY29uZHMpIHRoZW4gdGhlIHByb21pc2UgaXMgcmVqZWN0ZWQuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtzdHJpbmd9IG5hbWVcclxuICogQHBhcmFtIHthbnk9fSBhcmdzXHJcbiAqIEBwYXJhbSB7bnVtYmVyPX0gdGltZW91dFxyXG4gKiBAcmV0dXJuc1xyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENhbGwobmFtZSwgYXJncywgdGltZW91dCkge1xyXG5cclxuXHQvLyBUaW1lb3V0IGluZmluaXRlIGJ5IGRlZmF1bHRcclxuXHRpZiAodGltZW91dCA9PSBudWxsKSB7XHJcblx0XHR0aW1lb3V0ID0gMDtcclxuXHR9XHJcblxyXG5cdC8vIENyZWF0ZSBhIHByb21pc2VcclxuXHRyZXR1cm4gbmV3IFByb21pc2UoZnVuY3Rpb24gKHJlc29sdmUsIHJlamVjdCkge1xyXG5cclxuXHRcdC8vIENyZWF0ZSBhIHVuaXF1ZSBjYWxsYmFja0lEXHJcblx0XHR2YXIgY2FsbGJhY2tJRDtcclxuXHRcdGRvIHtcclxuXHRcdFx0Y2FsbGJhY2tJRCA9IG5hbWUgKyAnLScgKyByYW5kb21GdW5jKCk7XHJcblx0XHR9IHdoaWxlIChjYWxsYmFja3NbY2FsbGJhY2tJRF0pO1xyXG5cclxuXHRcdHZhciB0aW1lb3V0SGFuZGxlO1xyXG5cdFx0Ly8gU2V0IHRpbWVvdXRcclxuXHRcdGlmICh0aW1lb3V0ID4gMCkge1xyXG5cdFx0XHR0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XHJcblx0XHRcdFx0cmVqZWN0KEVycm9yKCdDYWxsIHRvICcgKyBuYW1lICsgJyB0aW1lZCBvdXQuIFJlcXVlc3QgSUQ6ICcgKyBjYWxsYmFja0lEKSk7XHJcblx0XHRcdH0sIHRpbWVvdXQpO1xyXG5cdFx0fVxyXG5cclxuXHRcdC8vIFN0b3JlIGNhbGxiYWNrXHJcblx0XHRjYWxsYmFja3NbY2FsbGJhY2tJRF0gPSB7XHJcblx0XHRcdHRpbWVvdXRIYW5kbGU6IHRpbWVvdXRIYW5kbGUsXHJcblx0XHRcdHJlamVjdDogcmVqZWN0LFxyXG5cdFx0XHRyZXNvbHZlOiByZXNvbHZlXHJcblx0XHR9O1xyXG5cclxuXHRcdHRyeSB7XHJcblx0XHRcdGNvbnN0IHBheWxvYWQgPSB7XHJcblx0XHRcdFx0bmFtZSxcclxuXHRcdFx0XHRhcmdzLFxyXG5cdFx0XHRcdGNhbGxiYWNrSUQsXHJcblx0XHRcdH07XHJcblxyXG4gICAgICAgICAgICAvLyBNYWtlIHRoZSBjYWxsXHJcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnQycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XHJcbiAgICAgICAgfSBjYXRjaCAoZSkge1xyXG4gICAgICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmVcclxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcclxuICAgICAgICB9XHJcbiAgICB9KTtcclxufVxyXG5cclxud2luZG93Lk9iZnVzY2F0ZWRDYWxsID0gKGlkLCBhcmdzLCB0aW1lb3V0KSA9PiB7XHJcblxyXG4gICAgLy8gVGltZW91dCBpbmZpbml0ZSBieSBkZWZhdWx0XHJcbiAgICBpZiAodGltZW91dCA9PSBudWxsKSB7XHJcbiAgICAgICAgdGltZW91dCA9IDA7XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQ3JlYXRlIGEgcHJvbWlzZVxyXG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKGZ1bmN0aW9uIChyZXNvbHZlLCByZWplY3QpIHtcclxuXHJcbiAgICAgICAgLy8gQ3JlYXRlIGEgdW5pcXVlIGNhbGxiYWNrSURcclxuICAgICAgICB2YXIgY2FsbGJhY2tJRDtcclxuICAgICAgICBkbyB7XHJcbiAgICAgICAgICAgIGNhbGxiYWNrSUQgPSBpZCArICctJyArIHJhbmRvbUZ1bmMoKTtcclxuICAgICAgICB9IHdoaWxlIChjYWxsYmFja3NbY2FsbGJhY2tJRF0pO1xyXG5cclxuICAgICAgICB2YXIgdGltZW91dEhhbmRsZTtcclxuICAgICAgICAvLyBTZXQgdGltZW91dFxyXG4gICAgICAgIGlmICh0aW1lb3V0ID4gMCkge1xyXG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XHJcbiAgICAgICAgICAgICAgICByZWplY3QoRXJyb3IoJ0NhbGwgdG8gbWV0aG9kICcgKyBpZCArICcgdGltZWQgb3V0LiBSZXF1ZXN0IElEOiAnICsgY2FsbGJhY2tJRCkpO1xyXG4gICAgICAgICAgICB9LCB0aW1lb3V0KTtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIC8vIFN0b3JlIGNhbGxiYWNrXHJcbiAgICAgICAgY2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xyXG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlOiB0aW1lb3V0SGFuZGxlLFxyXG4gICAgICAgICAgICByZWplY3Q6IHJlamVjdCxcclxuICAgICAgICAgICAgcmVzb2x2ZTogcmVzb2x2ZVxyXG4gICAgICAgIH07XHJcblxyXG4gICAgICAgIHRyeSB7XHJcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSB7XHJcblx0XHRcdFx0aWQsXHJcblx0XHRcdFx0YXJncyxcclxuXHRcdFx0XHRjYWxsYmFja0lELFxyXG5cdFx0XHR9O1xyXG5cclxuICAgICAgICAgICAgLy8gTWFrZSB0aGUgY2FsbFxyXG4gICAgICAgICAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ2MnICsgSlNPTi5zdHJpbmdpZnkocGF5bG9hZCkpO1xyXG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcclxuICAgICAgICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lXHJcbiAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoZSk7XHJcbiAgICAgICAgfVxyXG4gICAgfSk7XHJcbn07XHJcblxyXG5cclxuLyoqXHJcbiAqIENhbGxlZCBieSB0aGUgYmFja2VuZCB0byByZXR1cm4gZGF0YSB0byBhIHByZXZpb3VzbHkgY2FsbGVkXHJcbiAqIGJpbmRpbmcgaW52b2NhdGlvblxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBpbmNvbWluZ01lc3NhZ2VcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBDYWxsYmFjayhpbmNvbWluZ01lc3NhZ2UpIHtcclxuXHQvLyBQYXJzZSB0aGUgbWVzc2FnZVxyXG5cdGxldCBtZXNzYWdlO1xyXG5cdHRyeSB7XHJcblx0XHRtZXNzYWdlID0gSlNPTi5wYXJzZShpbmNvbWluZ01lc3NhZ2UpO1xyXG5cdH0gY2F0Y2ggKGUpIHtcclxuXHRcdGNvbnN0IGVycm9yID0gYEludmFsaWQgSlNPTiBwYXNzZWQgdG8gY2FsbGJhY2s6ICR7ZS5tZXNzYWdlfS4gTWVzc2FnZTogJHtpbmNvbWluZ01lc3NhZ2V9YDtcclxuXHRcdHJ1bnRpbWUuTG9nRGVidWcoZXJyb3IpO1xyXG5cdFx0dGhyb3cgbmV3IEVycm9yKGVycm9yKTtcclxuXHR9XHJcblx0bGV0IGNhbGxiYWNrSUQgPSBtZXNzYWdlLmNhbGxiYWNraWQ7XHJcblx0bGV0IGNhbGxiYWNrRGF0YSA9IGNhbGxiYWNrc1tjYWxsYmFja0lEXTtcclxuXHRpZiAoIWNhbGxiYWNrRGF0YSkge1xyXG5cdFx0Y29uc3QgZXJyb3IgPSBgQ2FsbGJhY2sgJyR7Y2FsbGJhY2tJRH0nIG5vdCByZWdpc3RlcmVkISEhYDtcclxuXHRcdGNvbnNvbGUuZXJyb3IoZXJyb3IpOyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lXHJcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xyXG5cdH1cclxuXHRjbGVhclRpbWVvdXQoY2FsbGJhY2tEYXRhLnRpbWVvdXRIYW5kbGUpO1xyXG5cclxuXHRkZWxldGUgY2FsbGJhY2tzW2NhbGxiYWNrSURdO1xyXG5cclxuXHRpZiAobWVzc2FnZS5lcnJvcikge1xyXG5cdFx0Y2FsbGJhY2tEYXRhLnJlamVjdChtZXNzYWdlLmVycm9yKTtcclxuXHR9IGVsc2Uge1xyXG5cdFx0Y2FsbGJhY2tEYXRhLnJlc29sdmUobWVzc2FnZS5yZXN1bHQpO1xyXG5cdH1cclxufVxyXG4iLCAiLypcclxuIF8gICAgICAgX18gICAgICBfIF9fICAgIFxyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApIFxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy8gIFxyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tICcuL2NhbGxzJztcclxuXHJcbi8vIFRoaXMgaXMgd2hlcmUgd2UgYmluZCBnbyBtZXRob2Qgd3JhcHBlcnNcclxud2luZG93LmdvID0ge307XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gU2V0QmluZGluZ3MoYmluZGluZ3NNYXApIHtcclxuXHR0cnkge1xyXG5cdFx0YmluZGluZ3NNYXAgPSBKU09OLnBhcnNlKGJpbmRpbmdzTWFwKTtcclxuXHR9IGNhdGNoIChlKSB7XHJcblx0XHRjb25zb2xlLmVycm9yKGUpO1xyXG5cdH1cclxuXHJcblx0Ly8gSW5pdGlhbGlzZSB0aGUgYmluZGluZ3MgbWFwXHJcblx0d2luZG93LmdvID0gd2luZG93LmdvIHx8IHt9O1xyXG5cclxuXHQvLyBJdGVyYXRlIHBhY2thZ2UgbmFtZXNcclxuXHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcCkuZm9yRWFjaCgocGFja2FnZU5hbWUpID0+IHtcclxuXHJcblx0XHQvLyBDcmVhdGUgaW5uZXIgbWFwIGlmIGl0IGRvZXNuJ3QgZXhpc3RcclxuXHRcdHdpbmRvdy5nb1twYWNrYWdlTmFtZV0gPSB3aW5kb3cuZ29bcGFja2FnZU5hbWVdIHx8IHt9O1xyXG5cclxuXHRcdC8vIEl0ZXJhdGUgc3RydWN0IG5hbWVzXHJcblx0XHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcFtwYWNrYWdlTmFtZV0pLmZvckVhY2goKHN0cnVjdE5hbWUpID0+IHtcclxuXHJcblx0XHRcdC8vIENyZWF0ZSBpbm5lciBtYXAgaWYgaXQgZG9lc24ndCBleGlzdFxyXG5cdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdID0gd2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSB8fCB7fTtcclxuXHJcblx0XHRcdE9iamVjdC5rZXlzKGJpbmRpbmdzTWFwW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSkuZm9yRWFjaCgobWV0aG9kTmFtZSkgPT4ge1xyXG5cclxuXHRcdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdW21ldGhvZE5hbWVdID0gZnVuY3Rpb24gKCkge1xyXG5cclxuXHRcdFx0XHRcdC8vIE5vIHRpbWVvdXQgYnkgZGVmYXVsdFxyXG5cdFx0XHRcdFx0bGV0IHRpbWVvdXQgPSAwO1xyXG5cclxuXHRcdFx0XHRcdC8vIEFjdHVhbCBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZnVuY3Rpb24gZHluYW1pYygpIHtcclxuXHRcdFx0XHRcdFx0Y29uc3QgYXJncyA9IFtdLnNsaWNlLmNhbGwoYXJndW1lbnRzKTtcclxuXHRcdFx0XHRcdFx0cmV0dXJuIENhbGwoW3BhY2thZ2VOYW1lLCBzdHJ1Y3ROYW1lLCBtZXRob2ROYW1lXS5qb2luKCcuJyksIGFyZ3MsIHRpbWVvdXQpO1xyXG5cdFx0XHRcdFx0fVxyXG5cclxuXHRcdFx0XHRcdC8vIEFsbG93IHNldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZHluYW1pYy5zZXRUaW1lb3V0ID0gZnVuY3Rpb24gKG5ld1RpbWVvdXQpIHtcclxuXHRcdFx0XHRcdFx0dGltZW91dCA9IG5ld1RpbWVvdXQ7XHJcblx0XHRcdFx0XHR9O1xyXG5cclxuXHRcdFx0XHRcdC8vIEFsbG93IGdldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZHluYW1pYy5nZXRUaW1lb3V0ID0gZnVuY3Rpb24gKCkge1xyXG5cdFx0XHRcdFx0XHRyZXR1cm4gdGltZW91dDtcclxuXHRcdFx0XHRcdH07XHJcblxyXG5cdFx0XHRcdFx0cmV0dXJuIGR5bmFtaWM7XHJcblx0XHRcdFx0fSgpO1xyXG5cdFx0XHR9KTtcclxuXHRcdH0pO1xyXG5cdH0pO1xyXG59XHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXHJcblxyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1JlbG9hZCgpIHtcclxuICAgIHdpbmRvdy5sb2NhdGlvbi5yZWxvYWQoKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1JlbG9hZEFwcCgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1InKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFN5c3RlbURlZmF1bHRUaGVtZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FTRFQnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldExpZ2h0VGhlbWUoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBTFQnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldERhcmtUaGVtZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FEVCcpO1xyXG59XHJcblxyXG4vKipcclxuICogUGxhY2UgdGhlIHdpbmRvdyBpbiB0aGUgY2VudGVyIG9mIHRoZSBzY3JlZW5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0NlbnRlcigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2MnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFNldHMgdGhlIHdpbmRvdyB0aXRsZVxyXG4gKlxyXG4gKiBAcGFyYW0ge3N0cmluZ30gdGl0bGVcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFRpdGxlKHRpdGxlKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dUJyArIHRpdGxlKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE1ha2VzIHRoZSB3aW5kb3cgZ28gZnVsbHNjcmVlblxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93RnVsbHNjcmVlbigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0YnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldmVydHMgdGhlIHdpbmRvdyBmcm9tIGZ1bGxzY3JlZW5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VuZnVsbHNjcmVlbigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2YnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIGluIGZ1bGwgc2NyZWVuIG1vZGUgb3Igbm90LlxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNGdWxsc2NyZWVuKCkge1xyXG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6V2luZG93SXNGdWxsc2NyZWVuXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcclxuICogQHBhcmFtIHtudW1iZXJ9IGhlaWdodFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFNpemUod2lkdGgsIGhlaWdodCkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXczonICsgd2lkdGggKyAnOicgKyBoZWlnaHQpO1xyXG59XHJcblxyXG4vKipcclxuICogR2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcmV0dXJuIHtQcm9taXNlPHt3OiBudW1iZXIsIGg6IG51bWJlcn0+fSBUaGUgc2l6ZSBvZiB0aGUgd2luZG93XHJcblxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0dldFNpemUoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRTaXplXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0IHRoZSBtYXhpbXVtIHNpemUgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB3aWR0aFxyXG4gKiBAcGFyYW0ge251bWJlcn0gaGVpZ2h0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0TWF4U2l6ZSh3aWR0aCwgaGVpZ2h0KSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1daOicgKyB3aWR0aCArICc6JyArIGhlaWdodCk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBTZXQgdGhlIG1pbmltdW0gc2l6ZSBvZiB0aGUgd2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtudW1iZXJ9IHdpZHRoXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRNaW5TaXplKHdpZHRoLCBoZWlnaHQpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3o6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcclxufVxyXG5cclxuXHJcblxyXG4vKipcclxuICogU2V0IHRoZSB3aW5kb3cgQWx3YXlzT25Ub3Agb3Igbm90IG9uIHRvcFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0QWx3YXlzT25Ub3AoYikge1xyXG5cclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FUUDonICsgKGIgPyAnMScgOiAnMCcpKTtcclxufVxyXG5cclxuXHJcblxyXG5cclxuLyoqXHJcbiAqIFNldCB0aGUgUG9zaXRpb24gb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0UG9zaXRpb24oeCwgeSkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcDonICsgeCArICc6JyArIHkpO1xyXG59XHJcblxyXG4vKipcclxuICogR2V0IHRoZSBQb3NpdGlvbiBvZiB0aGUgd2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTx7eDogbnVtYmVyLCB5OiBudW1iZXJ9Pn0gVGhlIHBvc2l0aW9uIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dHZXRQb3NpdGlvbigpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0dldFBvc1wiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIEhpZGUgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SGlkZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0gnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFNob3cgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2hvdygpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1MnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE1heGltaXNlIHRoZSBXaW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01heGltaXNlKCkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXTScpO1xyXG59XHJcblxyXG4vKipcclxuICogVG9nZ2xlIHRoZSBNYXhpbWlzZSBvZiB0aGUgV2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dUb2dnbGVNYXhpbWlzZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3QnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFVubWF4aW1pc2UgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93VW5tYXhpbWlzZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1UnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIG1heGltaXNlZCBvciBub3QuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc01heGltaXNlZCgpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0lzTWF4aW1pc2VkXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogTWluaW1pc2UgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93TWluaW1pc2UoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dtJyk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBVbm1pbmltaXNlIHRoZSBXaW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VubWluaW1pc2UoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d1Jyk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBtaW5pbWlzZWQgb3Igbm90LlxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNNaW5pbWlzZWQoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc01pbmltaXNlZFwiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIG5vcm1hbCBvciBub3QuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc05vcm1hbCgpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0lzTm9ybWFsXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0cyB0aGUgYmFja2dyb3VuZCBjb2xvdXIgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBSIFJlZFxyXG4gKiBAcGFyYW0ge251bWJlcn0gRyBHcmVlblxyXG4gKiBAcGFyYW0ge251bWJlcn0gQiBCbHVlXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBBIEFscGhhXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0QmFja2dyb3VuZENvbG91cihSLCBHLCBCLCBBKSB7XHJcbiAgICBsZXQgcmdiYSA9IEpTT04uc3RyaW5naWZ5KHtyOiBSIHx8IDAsIGc6IEcgfHwgMCwgYjogQiB8fCAwLCBhOiBBIHx8IDI1NX0pO1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcjonICsgcmdiYSk7XHJcbn1cclxuXHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXHJcblxyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xyXG5cclxuXHJcbi8qKlxyXG4gKiBHZXRzIHRoZSBhbGwgc2NyZWVucy4gQ2FsbCB0aGlzIGFuZXcgZWFjaCB0aW1lIHlvdSB3YW50IHRvIHJlZnJlc2ggZGF0YSBmcm9tIHRoZSB1bmRlcmx5aW5nIHdpbmRvd2luZyBzeXN0ZW0uXHJcbiAqIEBleHBvcnRcclxuICogQHR5cGVkZWYge2ltcG9ydCgnLi4vd3JhcHBlci9ydW50aW1lJykuU2NyZWVufSBTY3JlZW5cclxuICogQHJldHVybiB7UHJvbWlzZTx7U2NyZWVuW119Pn0gVGhlIHNjcmVlbnNcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBTY3JlZW5HZXRBbGwoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpTY3JlZW5HZXRBbGxcIik7XHJcbn1cclxuIiwgIi8qKlxyXG4gKiBAZGVzY3JpcHRpb246IFVzZSB0aGUgc3lzdGVtIGRlZmF1bHQgYnJvd3NlciB0byBvcGVuIHRoZSB1cmxcclxuICogQHBhcmFtIHtzdHJpbmd9IHVybCBcclxuICogQHJldHVybiB7dm9pZH1cclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBCcm93c2VyT3BlblVSTCh1cmwpIHtcclxuICB3aW5kb3cuV2FpbHNJbnZva2UoJ0JPOicgKyB1cmwpO1xyXG59IiwgIi8qXHJcbiBfXHQgICBfX1x0ICBfIF9fXHJcbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuXHJcbmltcG9ydCB7Q2FsbH0gZnJvbSBcIi4vY2FsbHNcIjtcclxuXHJcbi8qKlxyXG4gKiBTZXQgdGhlIFNpemUgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSB0ZXh0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gQ2xpcGJvYXJkU2V0VGV4dCh0ZXh0KSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpDbGlwYm9hcmRTZXRUZXh0XCIsIFt0ZXh0XSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBHZXQgdGhlIHRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTx7c3RyaW5nfT59IFRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXHJcblxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZEdldFRleHQoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpDbGlwYm9hcmRHZXRUZXh0XCIpO1xyXG59IiwgIi8qXHJcbiBfXHQgICBfX1x0ICBfIF9fXHJcbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuXHJcbmltcG9ydCB7RXZlbnRzT24sIEV2ZW50c09mZn0gZnJvbSBcIi4vZXZlbnRzXCI7XHJcblxyXG5jb25zdCBmbGFncyA9IHtcclxuICAgIHJlZ2lzdGVyZWQ6IGZhbHNlLFxyXG4gICAgZGVmYXVsdFVzZURyb3BUYXJnZXQ6IHRydWUsXHJcbiAgICB1c2VEcm9wVGFyZ2V0OiB0cnVlLFxyXG4gICAgbmV4dERlYWN0aXZhdGU6IG51bGwsXHJcbiAgICBuZXh0RGVhY3RpdmF0ZVRpbWVvdXQ6IG51bGwsXHJcbn07XHJcblxyXG5jb25zdCBEUk9QX1RBUkdFVF9BQ1RJVkUgPSBcIndhaWxzLWRyb3AtdGFyZ2V0LWFjdGl2ZVwiO1xyXG5cclxuLyoqXHJcbiAqIGNoZWNrU3R5bGVEcm9wVGFyZ2V0IGNoZWNrcyBpZiB0aGUgc3R5bGUgaGFzIHRoZSBkcm9wIHRhcmdldCBhdHRyaWJ1dGVcclxuICogXHJcbiAqIEBwYXJhbSB7Q1NTU3R5bGVEZWNsYXJhdGlvbn0gc3R5bGUgXHJcbiAqIEByZXR1cm5zIFxyXG4gKi9cclxuZnVuY3Rpb24gY2hlY2tTdHlsZURyb3BUYXJnZXQoc3R5bGUpIHtcclxuICAgIGNvbnN0IGNzc0Ryb3BWYWx1ZSA9IHN0eWxlLmdldFByb3BlcnR5VmFsdWUod2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BQcm9wZXJ0eSkudHJpbSgpO1xyXG4gICAgaWYgKGNzc0Ryb3BWYWx1ZSkge1xyXG4gICAgICAgIGlmIChjc3NEcm9wVmFsdWUgPT09IHdpbmRvdy53YWlscy5mbGFncy5jc3NEcm9wVmFsdWUpIHtcclxuICAgICAgICAgICAgcmV0dXJuIHRydWU7XHJcbiAgICAgICAgfVxyXG4gICAgICAgIC8vIGlmIHRoZSBlbGVtZW50IGhhcyB0aGUgZHJvcCB0YXJnZXQgYXR0cmlidXRlLCBidXQgXHJcbiAgICAgICAgLy8gdGhlIHZhbHVlIGlzIG5vdCBjb3JyZWN0LCB0ZXJtaW5hdGUgZmluZGluZyBwcm9jZXNzLlxyXG4gICAgICAgIC8vIFRoaXMgY2FuIGJlIHVzZWZ1bCB0byBibG9jayBzb21lIGNoaWxkIGVsZW1lbnRzIGZyb20gYmVpbmcgZHJvcCB0YXJnZXRzLlxyXG4gICAgICAgIHJldHVybiBmYWxzZTtcclxuICAgIH1cclxuICAgIHJldHVybiBmYWxzZTtcclxufVxyXG5cclxuLyoqXHJcbiAqIG9uRHJhZ092ZXIgaXMgY2FsbGVkIHdoZW4gdGhlIGRyYWdvdmVyIGV2ZW50IGlzIGVtaXR0ZWQuXHJcbiAqIEBwYXJhbSB7RHJhZ0V2ZW50fSBlXHJcbiAqIEByZXR1cm5zXHJcbiAqL1xyXG5mdW5jdGlvbiBvbkRyYWdPdmVyKGUpIHtcclxuICAgIC8vIENoZWNrIGlmIHRoaXMgaXMgYW4gZXh0ZXJuYWwgZmlsZSBkcm9wIG9yIGludGVybmFsIEhUTUwgZHJhZ1xyXG4gICAgLy8gRXh0ZXJuYWwgZmlsZSBkcm9wcyB3aWxsIGhhdmUgXCJGaWxlc1wiIGluIHRoZSB0eXBlcyBhcnJheVxyXG4gICAgLy8gSW50ZXJuYWwgSFRNTCBkcmFncyB0eXBpY2FsbHkgaGF2ZSBcInRleHQvcGxhaW5cIiwgXCJ0ZXh0L2h0bWxcIiBvciBjdXN0b20gdHlwZXNcclxuICAgIGNvbnN0IGlzRmlsZURyb3AgPSBlLmRhdGFUcmFuc2Zlci50eXBlcy5pbmNsdWRlcyhcIkZpbGVzXCIpO1xyXG5cclxuICAgIC8vIE9ubHkgaGFuZGxlIGV4dGVybmFsIGZpbGUgZHJvcHMsIGxldCBpbnRlcm5hbCBIVE1MNSBkcmFnLWFuZC1kcm9wIHdvcmsgbm9ybWFsbHlcclxuICAgIGlmICghaXNGaWxlRHJvcCkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuXHJcbiAgICAvLyBBTFdBWVMgcHJldmVudCBkZWZhdWx0IGZvciBmaWxlIGRyb3BzIHRvIHN0b3AgYnJvd3NlciBuYXZpZ2F0aW9uXHJcbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XHJcbiAgICBlLmRhdGFUcmFuc2Zlci5kcm9wRWZmZWN0ID0gJ2NvcHknO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKCFmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfVxyXG5cclxuICAgIGNvbnN0IGVsZW1lbnQgPSBlLnRhcmdldDtcclxuXHJcbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXHJcbiAgICBpZihmbGFncy5uZXh0RGVhY3RpdmF0ZSkgZmxhZ3MubmV4dERlYWN0aXZhdGUoKTtcclxuXHJcbiAgICAvLyBpZiB0aGUgZWxlbWVudCBpcyBudWxsIG9yIGVsZW1lbnQgaXMgbm90IGNoaWxkIG9mIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgIGlmICghZWxlbWVudCB8fCAhY2hlY2tTdHlsZURyb3BUYXJnZXQoZ2V0Q29tcHV0ZWRTdHlsZShlbGVtZW50KSkpIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgbGV0IGN1cnJlbnRFbGVtZW50ID0gZWxlbWVudDtcclxuICAgIHdoaWxlIChjdXJyZW50RWxlbWVudCkge1xyXG4gICAgICAgIC8vIGNoZWNrIGlmIGN1cnJlbnRFbGVtZW50IGlzIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgICAgICBpZiAoY2hlY2tTdHlsZURyb3BUYXJnZXQoZ2V0Q29tcHV0ZWRTdHlsZShjdXJyZW50RWxlbWVudCkpKSB7XHJcbiAgICAgICAgICAgIGN1cnJlbnRFbGVtZW50LmNsYXNzTGlzdC5hZGQoRFJPUF9UQVJHRVRfQUNUSVZFKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgY3VycmVudEVsZW1lbnQgPSBjdXJyZW50RWxlbWVudC5wYXJlbnRFbGVtZW50O1xyXG4gICAgfVxyXG59XHJcblxyXG4vKipcclxuICogb25EcmFnTGVhdmUgaXMgY2FsbGVkIHdoZW4gdGhlIGRyYWdsZWF2ZSBldmVudCBpcyBlbWl0dGVkLlxyXG4gKiBAcGFyYW0ge0RyYWdFdmVudH0gZVxyXG4gKiBAcmV0dXJuc1xyXG4gKi9cclxuZnVuY3Rpb24gb25EcmFnTGVhdmUoZSkge1xyXG4gICAgLy8gQ2hlY2sgaWYgdGhpcyBpcyBhbiBleHRlcm5hbCBmaWxlIGRyb3Agb3IgaW50ZXJuYWwgSFRNTCBkcmFnXHJcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcclxuXHJcbiAgICAvLyBPbmx5IGhhbmRsZSBleHRlcm5hbCBmaWxlIGRyb3BzLCBsZXQgaW50ZXJuYWwgSFRNTDUgZHJhZy1hbmQtZHJvcCB3b3JrIG5vcm1hbGx5XHJcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQUxXQVlTIHByZXZlbnQgZGVmYXVsdCBmb3IgZmlsZSBkcm9wcyB0byBzdG9wIGJyb3dzZXIgbmF2aWdhdGlvblxyXG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKCFmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfVxyXG5cclxuICAgIC8vIEZpbmQgdGhlIGNsb3NlIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgIGlmICghZS50YXJnZXQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZS50YXJnZXQpKSkge1xyXG4gICAgICAgIHJldHVybiBudWxsO1xyXG4gICAgfVxyXG5cclxuICAgIC8vIFRyaWdnZXIgZGVib3VuY2UgZnVuY3Rpb24gdG8gZGVhY3RpdmF0ZSBkcm9wIHRhcmdldHNcclxuICAgIGlmKGZsYWdzLm5leHREZWFjdGl2YXRlKSBmbGFncy5uZXh0RGVhY3RpdmF0ZSgpO1xyXG4gICAgXHJcbiAgICAvLyBVc2UgZGVib3VuY2UgdGVjaG5pcXVlIHRvIHRhY2xlIGRyYWdsZWF2ZSBldmVudHMgb24gb3ZlcmxhcHBpbmcgZWxlbWVudHMgYW5kIGRyb3AgdGFyZ2V0IGVsZW1lbnRzXHJcbiAgICBmbGFncy5uZXh0RGVhY3RpdmF0ZSA9ICgpID0+IHtcclxuICAgICAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHMsIG5ldyBkcm9wIHRhcmdldCB3aWxsIGJlIGFjdGl2YXRlZCBvbiBuZXh0IGRyYWdvdmVyIGV2ZW50XHJcbiAgICAgICAgQXJyYXkuZnJvbShkb2N1bWVudC5nZXRFbGVtZW50c0J5Q2xhc3NOYW1lKERST1BfVEFSR0VUX0FDVElWRSkpLmZvckVhY2goZWwgPT4gZWwuY2xhc3NMaXN0LnJlbW92ZShEUk9QX1RBUkdFVF9BQ1RJVkUpKTtcclxuICAgICAgICAvLyBSZXNldCBuZXh0RGVhY3RpdmF0ZVxyXG4gICAgICAgIGZsYWdzLm5leHREZWFjdGl2YXRlID0gbnVsbDtcclxuICAgICAgICAvLyBDbGVhciB0aW1lb3V0XHJcbiAgICAgICAgaWYgKGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCkge1xyXG4gICAgICAgICAgICBjbGVhclRpbWVvdXQoZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0KTtcclxuICAgICAgICAgICAgZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0ID0gbnVsbDtcclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgLy8gU2V0IHRpbWVvdXQgdG8gZGVhY3RpdmF0ZSBkcm9wIHRhcmdldHMgaWYgbm90IHRyaWdnZXJlZCBieSBuZXh0IGRyYWcgZXZlbnRcclxuICAgIGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCA9IHNldFRpbWVvdXQoKCkgPT4ge1xyXG4gICAgICAgIGlmKGZsYWdzLm5leHREZWFjdGl2YXRlKSBmbGFncy5uZXh0RGVhY3RpdmF0ZSgpO1xyXG4gICAgfSwgNTApO1xyXG59XHJcblxyXG4vKipcclxuICogb25Ecm9wIGlzIGNhbGxlZCB3aGVuIHRoZSBkcm9wIGV2ZW50IGlzIGVtaXR0ZWQuXHJcbiAqIEBwYXJhbSB7RHJhZ0V2ZW50fSBlXHJcbiAqIEByZXR1cm5zXHJcbiAqL1xyXG5mdW5jdGlvbiBvbkRyb3AoZSkge1xyXG4gICAgLy8gQ2hlY2sgaWYgdGhpcyBpcyBhbiBleHRlcm5hbCBmaWxlIGRyb3Agb3IgaW50ZXJuYWwgSFRNTCBkcmFnXHJcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcclxuXHJcbiAgICAvLyBPbmx5IGhhbmRsZSBleHRlcm5hbCBmaWxlIGRyb3BzLCBsZXQgaW50ZXJuYWwgSFRNTDUgZHJhZy1hbmQtZHJvcCB3b3JrIG5vcm1hbGx5XHJcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQUxXQVlTIHByZXZlbnQgZGVmYXVsdCBmb3IgZmlsZSBkcm9wcyB0byBzdG9wIGJyb3dzZXIgbmF2aWdhdGlvblxyXG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKENhblJlc29sdmVGaWxlUGF0aHMoKSkge1xyXG4gICAgICAgIC8vIHByb2Nlc3MgZmlsZXNcclxuICAgICAgICBsZXQgZmlsZXMgPSBbXTtcclxuICAgICAgICBpZiAoZS5kYXRhVHJhbnNmZXIuaXRlbXMpIHtcclxuICAgICAgICAgICAgZmlsZXMgPSBbLi4uZS5kYXRhVHJhbnNmZXIuaXRlbXNdLm1hcCgoaXRlbSwgaSkgPT4ge1xyXG4gICAgICAgICAgICAgICAgaWYgKGl0ZW0ua2luZCA9PT0gJ2ZpbGUnKSB7XHJcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIGl0ZW0uZ2V0QXNGaWxlKCk7XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH0pO1xyXG4gICAgICAgIH0gZWxzZSB7XHJcbiAgICAgICAgICAgIGZpbGVzID0gWy4uLmUuZGF0YVRyYW5zZmVyLmZpbGVzXTtcclxuICAgICAgICB9XHJcbiAgICAgICAgd2luZG93LnJ1bnRpbWUuUmVzb2x2ZUZpbGVQYXRocyhlLngsIGUueSwgZmlsZXMpO1xyXG4gICAgfVxyXG5cclxuICAgIGlmICghZmxhZ3MudXNlRHJvcFRhcmdldCkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuXHJcbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXHJcbiAgICBpZihmbGFncy5uZXh0RGVhY3RpdmF0ZSkgZmxhZ3MubmV4dERlYWN0aXZhdGUoKTtcclxuXHJcbiAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHNcclxuICAgIEFycmF5LmZyb20oZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZShEUk9QX1RBUkdFVF9BQ1RJVkUpKS5mb3JFYWNoKGVsID0+IGVsLmNsYXNzTGlzdC5yZW1vdmUoRFJPUF9UQVJHRVRfQUNUSVZFKSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBwb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0cyBjaGVja3MgdGhlIGJyb3dzZXIncyBjYXBhYmlsaXR5IG9mIHNlbmRpbmcgcG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHNcclxuICpcclxuICogQHJldHVybnMge2Jvb2xlYW59XHJcbiAqIEBjb25zdHJ1Y3RvclxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENhblJlc29sdmVGaWxlUGF0aHMoKSB7XHJcbiAgICByZXR1cm4gd2luZG93LmNocm9tZT8ud2Vidmlldz8ucG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHMgIT0gbnVsbDtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJlc29sdmVGaWxlUGF0aHMgc2VuZHMgZHJvcCBldmVudHMgdG8gdGhlIEdPIHNpZGUgdG8gcmVzb2x2ZSBmaWxlIHBhdGhzIG9uIHdpbmRvd3MuXHJcbiAqXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XHJcbiAqIEBwYXJhbSB7YW55W119IGZpbGVzXHJcbiAqIEBjb25zdHJ1Y3RvclxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFJlc29sdmVGaWxlUGF0aHMoeCwgeSwgZmlsZXMpIHtcclxuICAgIC8vIE9ubHkgZm9yIHdpbmRvd3Mgd2VidmlldzIgPj0gMS4wLjE3NzQuMzBcclxuICAgIC8vIGh0dHBzOi8vbGVhcm4ubWljcm9zb2Z0LmNvbS9lbi11cy9taWNyb3NvZnQtZWRnZS93ZWJ2aWV3Mi9yZWZlcmVuY2Uvd2luMzIvaWNvcmV3ZWJ2aWV3MndlYm1lc3NhZ2VyZWNlaXZlZGV2ZW50YXJnczI/dmlldz13ZWJ2aWV3Mi0xLjAuMTgyMy4zMiNhcHBsaWVzLXRvXHJcbiAgICBpZiAod2luZG93LmNocm9tZT8ud2Vidmlldz8ucG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHMpIHtcclxuICAgICAgICBjaHJvbWUud2Vidmlldy5wb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0cyhgZmlsZTpkcm9wOiR7eH06JHt5fWAsIGZpbGVzKTtcclxuICAgIH1cclxufVxyXG5cclxuLyoqXHJcbiAqIENhbGxiYWNrIGZvciBPbkZpbGVEcm9wIHJldHVybnMgYSBzbGljZSBvZiBmaWxlIHBhdGggc3RyaW5ncyB3aGVuIGEgZHJvcCBpcyBmaW5pc2hlZC5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAY2FsbGJhY2sgT25GaWxlRHJvcENhbGxiYWNrXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4IC0geCBjb29yZGluYXRlIG9mIHRoZSBkcm9wXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5IC0geSBjb29yZGluYXRlIG9mIHRoZSBkcm9wXHJcbiAqIEBwYXJhbSB7c3RyaW5nW119IHBhdGhzIC0gQSBsaXN0IG9mIGZpbGUgcGF0aHMuXHJcbiAqL1xyXG5cclxuLyoqXHJcbiAqIE9uRmlsZURyb3AgbGlzdGVucyB0byBkcmFnIGFuZCBkcm9wIGV2ZW50cyBhbmQgY2FsbHMgdGhlIGNhbGxiYWNrIHdpdGggdGhlIGNvb3JkaW5hdGVzIG9mIHRoZSBkcm9wIGFuZCBhbiBhcnJheSBvZiBwYXRoIHN0cmluZ3MuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtPbkZpbGVEcm9wQ2FsbGJhY2t9IGNhbGxiYWNrIC0gQ2FsbGJhY2sgZm9yIE9uRmlsZURyb3AgcmV0dXJucyBhIHNsaWNlIG9mIGZpbGUgcGF0aCBzdHJpbmdzIHdoZW4gYSBkcm9wIGlzIGZpbmlzaGVkLlxyXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFt1c2VEcm9wVGFyZ2V0PXRydWVdIC0gT25seSBjYWxsIHRoZSBjYWxsYmFjayB3aGVuIHRoZSBkcm9wIGZpbmlzaGVkIG9uIGFuIGVsZW1lbnQgdGhhdCBoYXMgdGhlIGRyb3AgdGFyZ2V0IHN0eWxlLiAoLS13YWlscy1kcm9wLXRhcmdldClcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBPbkZpbGVEcm9wKGNhbGxiYWNrLCB1c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICBpZiAodHlwZW9mIGNhbGxiYWNrICE9PSBcImZ1bmN0aW9uXCIpIHtcclxuICAgICAgICBjb25zb2xlLmVycm9yKFwiRHJhZ0FuZERyb3BDYWxsYmFjayBpcyBub3QgYSBmdW5jdGlvblwiKTtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGZsYWdzLnJlZ2lzdGVyZWQpIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcbiAgICBmbGFncy5yZWdpc3RlcmVkID0gdHJ1ZTtcclxuXHJcbiAgICBjb25zdCB1RFRQVCA9IHR5cGVvZiB1c2VEcm9wVGFyZ2V0O1xyXG4gICAgZmxhZ3MudXNlRHJvcFRhcmdldCA9IHVEVFBUID09PSBcInVuZGVmaW5lZFwiIHx8IHVEVFBUICE9PSBcImJvb2xlYW5cIiA/IGZsYWdzLmRlZmF1bHRVc2VEcm9wVGFyZ2V0IDogdXNlRHJvcFRhcmdldDtcclxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcmFnb3ZlcicsIG9uRHJhZ092ZXIpO1xyXG4gICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2RyYWdsZWF2ZScsIG9uRHJhZ0xlYXZlKTtcclxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcm9wJywgb25Ecm9wKTtcclxuXHJcbiAgICBsZXQgY2IgPSBjYWxsYmFjaztcclxuICAgIGlmIChmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgY2IgPSBmdW5jdGlvbiAoeCwgeSwgcGF0aHMpIHtcclxuICAgICAgICAgICAgY29uc3QgZWxlbWVudCA9IGRvY3VtZW50LmVsZW1lbnRGcm9tUG9pbnQoeCwgeSlcclxuICAgICAgICAgICAgLy8gaWYgdGhlIGVsZW1lbnQgaXMgbnVsbCBvciBlbGVtZW50IGlzIG5vdCBjaGlsZCBvZiBkcm9wIHRhcmdldCBlbGVtZW50LCByZXR1cm4gbnVsbFxyXG4gICAgICAgICAgICBpZiAoIWVsZW1lbnQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZWxlbWVudCkpKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm4gbnVsbDtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgICAgICBjYWxsYmFjayh4LCB5LCBwYXRocyk7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIEV2ZW50c09uKFwid2FpbHM6ZmlsZS1kcm9wXCIsIGNiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE9uRmlsZURyb3BPZmYgcmVtb3ZlcyB0aGUgZHJhZyBhbmQgZHJvcCBsaXN0ZW5lcnMgYW5kIGhhbmRsZXJzLlxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIE9uRmlsZURyb3BPZmYoKSB7XHJcbiAgICB3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcignZHJhZ292ZXInLCBvbkRyYWdPdmVyKTtcclxuICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdkcmFnbGVhdmUnLCBvbkRyYWdMZWF2ZSk7XHJcbiAgICB3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcignZHJvcCcsIG9uRHJvcCk7XHJcbiAgICBFdmVudHNPZmYoXCJ3YWlsczpmaWxlLWRyb3BcIik7XHJcbiAgICBmbGFncy5yZWdpc3RlcmVkID0gZmFsc2U7XHJcbn1cclxuIiwgIi8qXHJcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogYXV0bzsgKGRlZmF1bHQpIHdpbGwgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnUgaWYgY29udGVudEVkaXRhYmxlIGlzIHRydWUgT1IgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBPUiBlbGVtZW50IGlzIGlucHV0IG9yIHRleHRhcmVhXHJcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogc2hvdzsgd2lsbCBhbHdheXMgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnVcclxuLS1kZWZhdWx0LWNvbnRleHRtZW51OiBoaWRlOyB3aWxsIGFsd2F5cyBoaWRlIHRoZSBkZWZhdWx0IGNvbnRleHQgbWVudVxyXG5cclxuVGhpcyBydWxlIGlzIGluaGVyaXRlZCBsaWtlIG5vcm1hbCBDU1MgcnVsZXMsIHNvIG5lc3Rpbmcgd29ya3MgYXMgZXhwZWN0ZWRcclxuKi9cclxuZXhwb3J0IGZ1bmN0aW9uIHByb2Nlc3NEZWZhdWx0Q29udGV4dE1lbnUoZXZlbnQpIHtcclxuICAgIC8vIFByb2Nlc3MgZGVmYXVsdCBjb250ZXh0IG1lbnVcclxuICAgIGNvbnN0IGVsZW1lbnQgPSBldmVudC50YXJnZXQ7XHJcbiAgICBjb25zdCBjb21wdXRlZFN0eWxlID0gd2luZG93LmdldENvbXB1dGVkU3R5bGUoZWxlbWVudCk7XHJcbiAgICBjb25zdCBkZWZhdWx0Q29udGV4dE1lbnVBY3Rpb24gPSBjb21wdXRlZFN0eWxlLmdldFByb3BlcnR5VmFsdWUoXCItLWRlZmF1bHQtY29udGV4dG1lbnVcIikudHJpbSgpO1xyXG4gICAgc3dpdGNoIChkZWZhdWx0Q29udGV4dE1lbnVBY3Rpb24pIHtcclxuICAgICAgICBjYXNlIFwic2hvd1wiOlxyXG4gICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgY2FzZSBcImhpZGVcIjpcclxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcclxuICAgICAgICAgICAgcmV0dXJuO1xyXG4gICAgICAgIGRlZmF1bHQ6XHJcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIGNvbnRlbnRFZGl0YWJsZSBpcyB0cnVlXHJcbiAgICAgICAgICAgIGlmIChlbGVtZW50LmlzQ29udGVudEVkaXRhYmxlKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgICAgIH1cclxuXHJcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIHRleHQgaGFzIGJlZW4gc2VsZWN0ZWQgYW5kIGFjdGlvbiBpcyBvbiB0aGUgc2VsZWN0ZWQgZWxlbWVudHNcclxuICAgICAgICAgICAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xyXG4gICAgICAgICAgICBjb25zdCBoYXNTZWxlY3Rpb24gPSAoc2VsZWN0aW9uLnRvU3RyaW5nKCkubGVuZ3RoID4gMClcclxuICAgICAgICAgICAgaWYgKGhhc1NlbGVjdGlvbikge1xyXG4gICAgICAgICAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBzZWxlY3Rpb24ucmFuZ2VDb3VudDsgaSsrKSB7XHJcbiAgICAgICAgICAgICAgICAgICAgY29uc3QgcmFuZ2UgPSBzZWxlY3Rpb24uZ2V0UmFuZ2VBdChpKTtcclxuICAgICAgICAgICAgICAgICAgICBjb25zdCByZWN0cyA9IHJhbmdlLmdldENsaWVudFJlY3RzKCk7XHJcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgaiA9IDA7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xyXG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCByZWN0ID0gcmVjdHNbal07XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIGlmIChkb2N1bWVudC5lbGVtZW50RnJvbVBvaW50KHJlY3QubGVmdCwgcmVjdC50b3ApID09PSBlbGVtZW50KSB7XHJcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgLy8gQ2hlY2sgaWYgdGFnbmFtZSBpcyBpbnB1dCBvciB0ZXh0YXJlYVxyXG4gICAgICAgICAgICBpZiAoZWxlbWVudC50YWdOYW1lID09PSBcIklOUFVUXCIgfHwgZWxlbWVudC50YWdOYW1lID09PSBcIlRFWFRBUkVBXCIpIHtcclxuICAgICAgICAgICAgICAgIGlmIChoYXNTZWxlY3Rpb24gfHwgKCFlbGVtZW50LnJlYWRPbmx5ICYmICFlbGVtZW50LmRpc2FibGVkKSkge1xyXG4gICAgICAgICAgICAgICAgICAgIHJldHVybjtcclxuICAgICAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgfVxyXG5cclxuICAgICAgICAgICAgLy8gaGlkZSBkZWZhdWx0IGNvbnRleHQgbWVudVxyXG4gICAgICAgICAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpO1xyXG4gICAgfVxyXG59XHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuaW1wb3J0ICogYXMgTG9nIGZyb20gJy4vbG9nJztcclxuaW1wb3J0IHtcclxuICBldmVudExpc3RlbmVycyxcclxuICBFdmVudHNFbWl0LFxyXG4gIEV2ZW50c05vdGlmeSxcclxuICBFdmVudHNPZmYsXHJcbiAgRXZlbnRzT2ZmQWxsLFxyXG4gIEV2ZW50c09uLFxyXG4gIEV2ZW50c09uY2UsXHJcbiAgRXZlbnRzT25NdWx0aXBsZSxcclxufSBmcm9tIFwiLi9ldmVudHNcIjtcclxuaW1wb3J0IHsgQ2FsbCwgQ2FsbGJhY2ssIGNhbGxiYWNrcyB9IGZyb20gJy4vY2FsbHMnO1xyXG5pbXBvcnQgeyBTZXRCaW5kaW5ncyB9IGZyb20gXCIuL2JpbmRpbmdzXCI7XHJcbmltcG9ydCAqIGFzIFdpbmRvdyBmcm9tIFwiLi93aW5kb3dcIjtcclxuaW1wb3J0ICogYXMgU2NyZWVuIGZyb20gXCIuL3NjcmVlblwiO1xyXG5pbXBvcnQgKiBhcyBCcm93c2VyIGZyb20gXCIuL2Jyb3dzZXJcIjtcclxuaW1wb3J0ICogYXMgQ2xpcGJvYXJkIGZyb20gXCIuL2NsaXBib2FyZFwiO1xyXG5pbXBvcnQgKiBhcyBEcmFnQW5kRHJvcCBmcm9tIFwiLi9kcmFnYW5kZHJvcFwiO1xyXG5pbXBvcnQgKiBhcyBDb250ZXh0TWVudSBmcm9tIFwiLi9jb250ZXh0bWVudVwiO1xyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFF1aXQoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1EnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFNob3coKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1MnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIEhpZGUoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ0gnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIEVudmlyb25tZW50KCkge1xyXG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6RW52aXJvbm1lbnRcIik7XHJcbn1cclxuXHJcbi8vIFRoZSBKUyBydW50aW1lXHJcbndpbmRvdy5ydW50aW1lID0ge1xyXG4gICAgLi4uTG9nLFxyXG4gICAgLi4uV2luZG93LFxyXG4gICAgLi4uQnJvd3NlcixcclxuICAgIC4uLlNjcmVlbixcclxuICAgIC4uLkNsaXBib2FyZCxcclxuICAgIC4uLkRyYWdBbmREcm9wLFxyXG4gICAgRXZlbnRzT24sXHJcbiAgICBFdmVudHNPbmNlLFxyXG4gICAgRXZlbnRzT25NdWx0aXBsZSxcclxuICAgIEV2ZW50c0VtaXQsXHJcbiAgICBFdmVudHNPZmYsXHJcbiAgICBFdmVudHNPZmZBbGwsXHJcbiAgICBFbnZpcm9ubWVudCxcclxuICAgIFNob3csXHJcbiAgICBIaWRlLFxyXG4gICAgUXVpdFxyXG59O1xyXG5cclxuLy8gSW50ZXJuYWwgd2FpbHMgZW5kcG9pbnRzXHJcbndpbmRvdy53YWlscyA9IHtcclxuICAgIENhbGxiYWNrLFxyXG4gICAgRXZlbnRzTm90aWZ5LFxyXG4gICAgU2V0QmluZGluZ3MsXHJcbiAgICBldmVudExpc3RlbmVycyxcclxuICAgIGNhbGxiYWNrcyxcclxuICAgIGZsYWdzOiB7XHJcbiAgICAgICAgZGlzYWJsZVNjcm9sbGJhckRyYWc6IGZhbHNlLFxyXG4gICAgICAgIGRpc2FibGVEZWZhdWx0Q29udGV4dE1lbnU6IGZhbHNlLFxyXG4gICAgICAgIGVuYWJsZVJlc2l6ZTogZmFsc2UsXHJcbiAgICAgICAgZGVmYXVsdEN1cnNvcjogbnVsbCxcclxuICAgICAgICBib3JkZXJUaGlja25lc3M6IDYsXHJcbiAgICAgICAgc2hvdWxkRHJhZzogZmFsc2UsXHJcbiAgICAgICAgZGVmZXJEcmFnVG9Nb3VzZU1vdmU6IHRydWUsXHJcbiAgICAgICAgY3NzRHJhZ1Byb3BlcnR5OiBcIi0td2FpbHMtZHJhZ2dhYmxlXCIsXHJcbiAgICAgICAgY3NzRHJhZ1ZhbHVlOiBcImRyYWdcIixcclxuICAgICAgICBjc3NEcm9wUHJvcGVydHk6IFwiLS13YWlscy1kcm9wLXRhcmdldFwiLFxyXG4gICAgICAgIGNzc0Ryb3BWYWx1ZTogXCJkcm9wXCIsXHJcbiAgICAgICAgZW5hYmxlV2FpbHNEcmFnQW5kRHJvcDogZmFsc2UsXHJcbiAgICB9XHJcbn07XHJcblxyXG4vLyBTZXQgdGhlIGJpbmRpbmdzXHJcbmlmICh3aW5kb3cud2FpbHNiaW5kaW5ncykge1xyXG4gICAgd2luZG93LndhaWxzLlNldEJpbmRpbmdzKHdpbmRvdy53YWlsc2JpbmRpbmdzKTtcclxuICAgIGRlbGV0ZSB3aW5kb3cud2FpbHMuU2V0QmluZGluZ3M7XHJcbn1cclxuXHJcbi8vIChib29sKSBUaGlzIGlzIGV2YWx1YXRlZCBhdCBidWlsZCB0aW1lIGluIHBhY2thZ2UuanNvblxyXG5pZiAoIURFQlVHKSB7XHJcbiAgICBkZWxldGUgd2luZG93LndhaWxzYmluZGluZ3M7XHJcbn1cclxuXHJcbmxldCBkcmFnVGVzdCA9IGZ1bmN0aW9uKGUpIHtcclxuICAgIHZhciB2YWwgPSB3aW5kb3cuZ2V0Q29tcHV0ZWRTdHlsZShlLnRhcmdldCkuZ2V0UHJvcGVydHlWYWx1ZSh3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5KTtcclxuICAgIGlmICh2YWwpIHtcclxuICAgICAgICB2YWwgPSB2YWwudHJpbSgpO1xyXG4gICAgfVxyXG5cclxuICAgIGlmICh2YWwgIT09IHdpbmRvdy53YWlscy5mbGFncy5jc3NEcmFnVmFsdWUpIHtcclxuICAgICAgICByZXR1cm4gZmFsc2U7XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGUuYnV0dG9ucyAhPT0gMSkge1xyXG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBub3QgdGhlIHByaW1hcnkgYnV0dG9uIGhhcyBiZWVuIGNsaWNrZWQuXHJcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xyXG4gICAgfVxyXG5cclxuICAgIGlmIChlLmRldGFpbCAhPT0gMSkge1xyXG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBtb3JlIHRoYW4gb25jZSBoYXMgYmVlbiBjbGlja2VkLCBlLmcuIHdoZW4gZG91YmxlIGNsaWNraW5nXHJcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiB0cnVlO1xyXG59O1xyXG5cclxud2luZG93LndhaWxzLnNldENTU0RyYWdQcm9wZXJ0aWVzID0gZnVuY3Rpb24ocHJvcGVydHksIHZhbHVlKSB7XHJcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5ID0gcHJvcGVydHk7XHJcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1ZhbHVlID0gdmFsdWU7XHJcbn1cclxuXHJcbndpbmRvdy53YWlscy5zZXRDU1NEcm9wUHJvcGVydGllcyA9IGZ1bmN0aW9uKHByb3BlcnR5LCB2YWx1ZSkge1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BQcm9wZXJ0eSA9IHByb3BlcnR5O1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BWYWx1ZSA9IHZhbHVlO1xyXG59XHJcblxyXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignbW91c2Vkb3duJywgKGUpID0+IHtcclxuICAgIC8vIENoZWNrIGZvciByZXNpemluZ1xyXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlKSB7XHJcbiAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwicmVzaXplOlwiICsgd2luZG93LndhaWxzLmZsYWdzLnJlc2l6ZUVkZ2UpO1xyXG4gICAgICAgIGUucHJldmVudERlZmF1bHQoKTtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGRyYWdUZXN0KGUpKSB7XHJcbiAgICAgICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kaXNhYmxlU2Nyb2xsYmFyRHJhZykge1xyXG4gICAgICAgICAgICAvLyBUaGlzIGNoZWNrcyBmb3IgY2xpY2tzIG9uIHRoZSBzY3JvbGwgYmFyXHJcbiAgICAgICAgICAgIGlmIChlLm9mZnNldFggPiBlLnRhcmdldC5jbGllbnRXaWR0aCB8fCBlLm9mZnNldFkgPiBlLnRhcmdldC5jbGllbnRIZWlnaHQpIHtcclxuICAgICAgICAgICAgICAgIHJldHVybjtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgICAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLmRlZmVyRHJhZ1RvTW91c2VNb3ZlKSB7XHJcbiAgICAgICAgICAgIHdpbmRvdy53YWlscy5mbGFncy5zaG91bGREcmFnID0gdHJ1ZTtcclxuICAgICAgICB9IGVsc2Uge1xyXG4gICAgICAgICAgICBlLnByZXZlbnREZWZhdWx0KClcclxuICAgICAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwiZHJhZ1wiKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfSBlbHNlIHtcclxuICAgICAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xyXG4gICAgfVxyXG59KTtcclxuXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgKCkgPT4ge1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcclxufSk7XHJcblxyXG5mdW5jdGlvbiBzZXRSZXNpemUoY3Vyc29yKSB7XHJcbiAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yID0gY3Vyc29yIHx8IHdpbmRvdy53YWlscy5mbGFncy5kZWZhdWx0Q3Vyc29yO1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLnJlc2l6ZUVkZ2UgPSBjdXJzb3I7XHJcbn1cclxuXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW1vdmUnLCBmdW5jdGlvbihlKSB7XHJcbiAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcpIHtcclxuICAgICAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xyXG4gICAgICAgIGxldCBtb3VzZVByZXNzZWQgPSBlLmJ1dHRvbnMgIT09IHVuZGVmaW5lZCA/IGUuYnV0dG9ucyA6IGUud2hpY2g7XHJcbiAgICAgICAgaWYgKG1vdXNlUHJlc3NlZCA+IDApIHtcclxuICAgICAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwiZHJhZ1wiKTtcclxuICAgICAgICAgICAgcmV0dXJuO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVJlc2l6ZSkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuICAgIGlmICh3aW5kb3cud2FpbHMuZmxhZ3MuZGVmYXVsdEN1cnNvciA9PSBudWxsKSB7XHJcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLmRlZmF1bHRDdXJzb3IgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yO1xyXG4gICAgfVxyXG4gICAgaWYgKHdpbmRvdy5vdXRlcldpZHRoIC0gZS5jbGllbnRYIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcyAmJiB3aW5kb3cub3V0ZXJIZWlnaHQgLSBlLmNsaWVudFkgPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzKSB7XHJcbiAgICAgICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmN1cnNvciA9IFwic2UtcmVzaXplXCI7XHJcbiAgICB9XHJcbiAgICBsZXQgcmlnaHRCb3JkZXIgPSB3aW5kb3cub3V0ZXJXaWR0aCAtIGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XHJcbiAgICBsZXQgbGVmdEJvcmRlciA9IGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XHJcbiAgICBsZXQgdG9wQm9yZGVyID0gZS5jbGllbnRZIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcztcclxuICAgIGxldCBib3R0b21Cb3JkZXIgPSB3aW5kb3cub3V0ZXJIZWlnaHQgLSBlLmNsaWVudFkgPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzO1xyXG5cclxuICAgIC8vIElmIHdlIGFyZW4ndCBvbiBhbiBlZGdlLCBidXQgd2VyZSwgcmVzZXQgdGhlIGN1cnNvciB0byBkZWZhdWx0XHJcbiAgICBpZiAoIWxlZnRCb3JkZXIgJiYgIXJpZ2h0Qm9yZGVyICYmICF0b3BCb3JkZXIgJiYgIWJvdHRvbUJvcmRlciAmJiB3aW5kb3cud2FpbHMuZmxhZ3MucmVzaXplRWRnZSAhPT0gdW5kZWZpbmVkKSB7XHJcbiAgICAgICAgc2V0UmVzaXplKCk7XHJcbiAgICB9IGVsc2UgaWYgKHJpZ2h0Qm9yZGVyICYmIGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwic2UtcmVzaXplXCIpO1xyXG4gICAgZWxzZSBpZiAobGVmdEJvcmRlciAmJiBib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInN3LXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIgJiYgdG9wQm9yZGVyKSBzZXRSZXNpemUoXCJudy1yZXNpemVcIik7XHJcbiAgICBlbHNlIGlmICh0b3BCb3JkZXIgJiYgcmlnaHRCb3JkZXIpIHNldFJlc2l6ZShcIm5lLXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIpIHNldFJlc2l6ZShcInctcmVzaXplXCIpO1xyXG4gICAgZWxzZSBpZiAodG9wQm9yZGVyKSBzZXRSZXNpemUoXCJuLXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwicy1yZXNpemVcIik7XHJcbiAgICBlbHNlIGlmIChyaWdodEJvcmRlcikgc2V0UmVzaXplKFwiZS1yZXNpemVcIik7XHJcblxyXG59KTtcclxuXHJcbi8vIFNldHVwIGNvbnRleHQgbWVudSBob29rXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdjb250ZXh0bWVudScsIGZ1bmN0aW9uKGUpIHtcclxuICAgIC8vIGFsd2F5cyBzaG93IHRoZSBjb250ZXh0bWVudSBpbiBkZWJ1ZyAmIGRldlxyXG4gICAgaWYgKERFQlVHKSByZXR1cm47XHJcblxyXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kaXNhYmxlRGVmYXVsdENvbnRleHRNZW51KSB7XHJcbiAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG4gICAgfSBlbHNlIHtcclxuICAgICAgICBDb250ZXh0TWVudS5wcm9jZXNzRGVmYXVsdENvbnRleHRNZW51KGUpO1xyXG4gICAgfVxyXG59KTtcclxuXHJcbndpbmRvdy5XYWlsc0ludm9rZShcInJ1bnRpbWU6cmVhZHlcIik7Il0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFrQkEsV0FBUyxlQUFlLE9BQU8sU0FBUztBQUl2QyxXQUFPLFlBQVksTUFBTSxRQUFRLE9BQU87QUFBQSxFQUN6QztBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxTQUFTLFNBQVM7QUFDakMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsUUFBUSxTQUFTO0FBQ2hDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxXQUFXLFNBQVM7QUFDbkMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxZQUFZLFVBQVU7QUFDckMsbUJBQWUsS0FBSyxRQUFRO0FBQUEsRUFDN0I7QUFHTyxNQUFNLFdBQVc7QUFBQSxJQUN2QixPQUFPO0FBQUEsSUFDUCxPQUFPO0FBQUEsSUFDUCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxPQUFPO0FBQUEsRUFDUjs7O0FDOUZBLE1BQU0sV0FBTixNQUFlO0FBQUEsSUFRWCxZQUFZLFdBQVcsVUFBVSxjQUFjO0FBQzNDLFdBQUssWUFBWTtBQUVqQixXQUFLLGVBQWUsZ0JBQWdCO0FB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 diff --git a/v2/internal/frontend/runtime/runtime_prod_desktop.go b/v2/internal/frontend/runtime/runtime_prod_desktop.go new file mode 100644 index 000000000..7336f0102 --- /dev/null +++ b/v2/internal/frontend/runtime/runtime_prod_desktop.go @@ -0,0 +1,8 @@ +//go:build production && !debug + +package runtime + +import _ "embed" + +//go:embed runtime_prod_desktop.js +var RuntimeDesktopJS []byte diff --git a/v2/internal/frontend/runtime/runtime_prod_desktop.js b/v2/internal/frontend/runtime/runtime_prod_desktop.js new file mode 100644 index 000000000..3d38924f7 --- /dev/null +++ b/v2/internal/frontend/runtime/runtime_prod_desktop.js @@ -0,0 +1 @@ +(()=>{var j=Object.defineProperty;var p=(e,t)=>{for(var n in t)j(e,n,{get:t[n],enumerable:!0})};var b={};p(b,{LogDebug:()=>$,LogError:()=>Q,LogFatal:()=>_,LogInfo:()=>Y,LogLevel:()=>K,LogPrint:()=>X,LogTrace:()=>J,LogWarning:()=>q,SetLogLevel:()=>Z});function u(e,t){window.WailsInvoke("L"+e+t)}function J(e){u("T",e)}function X(e){u("P",e)}function $(e){u("D",e)}function Y(e){u("I",e)}function q(e){u("W",e)}function Q(e){u("E",e)}function _(e){u("F",e)}function Z(e){u("S",e)}var K={TRACE:1,DEBUG:2,INFO:3,WARNING:4,ERROR:5};var y=class{constructor(t,n,o){this.eventName=t,this.maxCallbacks=o||-1,this.Callback=i=>(n.apply(null,i),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}},w={};function v(e,t,n){w[e]=w[e]||[];let o=new y(e,t,n);return w[e].push(o),()=>ee(o)}function W(e,t){return v(e,t,-1)}function A(e,t){return v(e,t,1)}function P(e){let t=e.name,n=w[t]?.slice()||[];if(n.length){for(let o=n.length-1;o>=0;o-=1){let i=n[o],r=e.data;i.Callback(r)&&n.splice(o,1)}n.length===0?g(t):w[t]=n}}function F(e){let t;try{t=JSON.parse(e)}catch{let o="Invalid JSON passed to Notify: "+e;throw new Error(o)}P(t)}function R(e){let t={name:e,data:[].slice.apply(arguments).slice(1)};P(t),window.WailsInvoke("EE"+JSON.stringify(t))}function g(e){delete w[e],window.WailsInvoke("EX"+e)}function x(e,...t){g(e),t.length>0&&t.forEach(n=>{g(n)})}function M(){Object.keys(w).forEach(t=>{g(t)})}function ee(e){let t=e.eventName;w[t]!==void 0&&(w[t]=w[t].filter(n=>n!==e),w[t].length===0&&g(t))}var c={};function te(){var e=new Uint32Array(1);return window.crypto.getRandomValues(e)[0]}function ne(){return Math.random()*9007199254740991}var D;window.crypto?D=te:D=ne;function a(e,t,n){return n==null&&(n=0),new Promise(function(o,i){var r;do r=e+"-"+D();while(c[r]);var l;n>0&&(l=setTimeout(function(){i(Error("Call to "+e+" timed out. Request ID: "+r))},n)),c[r]={timeoutHandle:l,reject:i,resolve:o};try{let d={name:e,args:t,callbackID:r};window.WailsInvoke("C"+JSON.stringify(d))}catch(d){console.error(d)}})}window.ObfuscatedCall=(e,t,n)=>(n==null&&(n=0),new Promise(function(o,i){var r;do r=e+"-"+D();while(c[r]);var l;n>0&&(l=setTimeout(function(){i(Error("Call to method "+e+" timed out. Request ID: "+r))},n)),c[r]={timeoutHandle:l,reject:i,resolve:o};try{let d={id:e,args:t,callbackID:r};window.WailsInvoke("c"+JSON.stringify(d))}catch(d){console.error(d)}}));function z(e){let t;try{t=JSON.parse(e)}catch(i){let r=`Invalid JSON passed to callback: ${i.message}. Message: ${e}`;throw runtime.LogDebug(r),new Error(r)}let n=t.callbackid,o=c[n];if(!o){let i=`Callback '${n}' not registered!!!`;throw console.error(i),new Error(i)}clearTimeout(o.timeoutHandle),delete c[n],t.error?o.reject(t.error):o.resolve(t.result)}window.go={};function B(e){try{e=JSON.parse(e)}catch(t){console.error(t)}window.go=window.go||{},Object.keys(e).forEach(t=>{window.go[t]=window.go[t]||{},Object.keys(e[t]).forEach(n=>{window.go[t][n]=window.go[t][n]||{},Object.keys(e[t][n]).forEach(o=>{window.go[t][n][o]=function(){let i=0;function r(){let l=[].slice.call(arguments);return a([t,n,o].join("."),l,i)}return r.setTimeout=function(l){i=l},r.getTimeout=function(){return i},r}()})})})}var T={};p(T,{WindowCenter:()=>ae,WindowFullscreen:()=>de,WindowGetPosition:()=>xe,WindowGetSize:()=>pe,WindowHide:()=>De,WindowIsFullscreen:()=>ue,WindowIsMaximised:()=>Te,WindowIsMinimised:()=>Ce,WindowIsNormal:()=>Ie,WindowMaximise:()=>Ee,WindowMinimise:()=>Se,WindowReload:()=>oe,WindowReloadApp:()=>ie,WindowSetAlwaysOnTop:()=>ve,WindowSetBackgroundColour:()=>Oe,WindowSetDarkTheme:()=>le,WindowSetLightTheme:()=>se,WindowSetMaxSize:()=>ge,WindowSetMinSize:()=>me,WindowSetPosition:()=>We,WindowSetSize:()=>ce,WindowSetSystemDefaultTheme:()=>re,WindowSetTitle:()=>we,WindowShow:()=>he,WindowToggleMaximise:()=>be,WindowUnfullscreen:()=>fe,WindowUnmaximise:()=>ye,WindowUnminimise:()=>ke});function oe(){window.location.reload()}function ie(){window.WailsInvoke("WR")}function re(){window.WailsInvoke("WASDT")}function se(){window.WailsInvoke("WALT")}function le(){window.WailsInvoke("WADT")}function ae(){window.WailsInvoke("Wc")}function we(e){window.WailsInvoke("WT"+e)}function de(){window.WailsInvoke("WF")}function fe(){window.WailsInvoke("Wf")}function ue(){return a(":wails:WindowIsFullscreen")}function ce(e,t){window.WailsInvoke("Ws:"+e+":"+t)}function pe(){return a(":wails:WindowGetSize")}function ge(e,t){window.WailsInvoke("WZ:"+e+":"+t)}function me(e,t){window.WailsInvoke("Wz:"+e+":"+t)}function ve(e){window.WailsInvoke("WATP:"+(e?"1":"0"))}function We(e,t){window.WailsInvoke("Wp:"+e+":"+t)}function xe(){return a(":wails:WindowGetPos")}function De(){window.WailsInvoke("WH")}function he(){window.WailsInvoke("WS")}function Ee(){window.WailsInvoke("WM")}function be(){window.WailsInvoke("Wt")}function ye(){window.WailsInvoke("WU")}function Te(){return a(":wails:WindowIsMaximised")}function Se(){window.WailsInvoke("Wm")}function ke(){window.WailsInvoke("Wu")}function Ce(){return a(":wails:WindowIsMinimised")}function Ie(){return a(":wails:WindowIsNormal")}function Oe(e,t,n,o){let i=JSON.stringify({r:e||0,g:t||0,b:n||0,a:o||255});window.WailsInvoke("Wr:"+i)}var S={};p(S,{ScreenGetAll:()=>Le});function Le(){return a(":wails:ScreenGetAll")}var k={};p(k,{BrowserOpenURL:()=>Ae});function Ae(e){window.WailsInvoke("BO:"+e)}var C={};p(C,{ClipboardGetText:()=>Fe,ClipboardSetText:()=>Pe});function Pe(e){return a(":wails:ClipboardSetText",[e])}function Fe(){return a(":wails:ClipboardGetText")}var I={};p(I,{CanResolveFilePaths:()=>V,OnFileDrop:()=>Me,OnFileDropOff:()=>ze,ResolveFilePaths:()=>Re});var s={registered:!1,defaultUseDropTarget:!0,useDropTarget:!0,nextDeactivate:null,nextDeactivateTimeout:null},m="wails-drop-target-active";function h(e){let t=e.getPropertyValue(window.wails.flags.cssDropProperty).trim();return t?t===window.wails.flags.cssDropValue:!1}function G(e){if(!e.dataTransfer.types.includes("Files")||(e.preventDefault(),e.dataTransfer.dropEffect="copy",!window.wails.flags.enableWailsDragAndDrop)||!s.useDropTarget)return;let n=e.target;if(s.nextDeactivate&&s.nextDeactivate(),!n||!h(getComputedStyle(n)))return;let o=n;for(;o;)h(getComputedStyle(o))&&o.classList.add(m),o=o.parentElement}function H(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop&&!!s.useDropTarget)){if(!e.target||!h(getComputedStyle(e.target)))return null;s.nextDeactivate&&s.nextDeactivate(),s.nextDeactivate=()=>{Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)),s.nextDeactivate=null,s.nextDeactivateTimeout&&(clearTimeout(s.nextDeactivateTimeout),s.nextDeactivateTimeout=null)},s.nextDeactivateTimeout=setTimeout(()=>{s.nextDeactivate&&s.nextDeactivate()},50)}}function U(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop)){if(V()){let n=[];e.dataTransfer.items?n=[...e.dataTransfer.items].map((o,i)=>{if(o.kind==="file")return o.getAsFile()}):n=[...e.dataTransfer.files],window.runtime.ResolveFilePaths(e.x,e.y,n)}!s.useDropTarget||(s.nextDeactivate&&s.nextDeactivate(),Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)))}}function V(){return window.chrome?.webview?.postMessageWithAdditionalObjects!=null}function Re(e,t,n){window.chrome?.webview?.postMessageWithAdditionalObjects&&chrome.webview.postMessageWithAdditionalObjects(`file:drop:${e}:${t}`,n)}function Me(e,t){if(typeof e!="function"){console.error("DragAndDropCallback is not a function");return}if(s.registered)return;s.registered=!0;let n=typeof t;s.useDropTarget=n==="undefined"||n!=="boolean"?s.defaultUseDropTarget:t,window.addEventListener("dragover",G),window.addEventListener("dragleave",H),window.addEventListener("drop",U);let o=e;s.useDropTarget&&(o=function(i,r,l){let d=document.elementFromPoint(i,r);if(!d||!h(getComputedStyle(d)))return null;e(i,r,l)}),W("wails:file-drop",o)}function ze(){window.removeEventListener("dragover",G),window.removeEventListener("dragleave",H),window.removeEventListener("drop",U),x("wails:file-drop"),s.registered=!1}function N(e){let t=e.target;switch(window.getComputedStyle(t).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return;default:if(t.isContentEditable)return;let i=window.getSelection(),r=i.toString().length>0;if(r)for(let l=0;l{if(window.wails.flags.resizeEdge){window.WailsInvoke("resize:"+window.wails.flags.resizeEdge),e.preventDefault();return}if(Ne(e)){if(window.wails.flags.disableScrollbarDrag&&(e.offsetX>e.target.clientWidth||e.offsetY>e.target.clientHeight))return;window.wails.flags.deferDragToMouseMove?window.wails.flags.shouldDrag=!0:(e.preventDefault(),window.WailsInvoke("drag"));return}else window.wails.flags.shouldDrag=!1});window.addEventListener("mouseup",()=>{window.wails.flags.shouldDrag=!1});function f(e){document.documentElement.style.cursor=e||window.wails.flags.defaultCursor,window.wails.flags.resizeEdge=e}window.addEventListener("mousemove",function(e){if(window.wails.flags.shouldDrag&&(window.wails.flags.shouldDrag=!1,(e.buttons!==void 0?e.buttons:e.which)>0)){window.WailsInvoke("drag");return}if(!window.wails.flags.enableResize)return;window.wails.flags.defaultCursor==null&&(window.wails.flags.defaultCursor=document.documentElement.style.cursor),window.outerWidth-e.clientX", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/internal/frontend/runtime/wrapper/runtime.d.ts b/v2/internal/frontend/runtime/wrapper/runtime.d.ts new file mode 100644 index 000000000..4445dac21 --- /dev/null +++ b/v2/internal/frontend/runtime/wrapper/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/v2/internal/frontend/runtime/wrapper/runtime.js b/v2/internal/frontend/runtime/wrapper/runtime.js new file mode 100644 index 000000000..7cb89d750 --- /dev/null +++ b/v2/internal/frontend/runtime/wrapper/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/v2/internal/frontend/runtime/wrapper/wrapper.go b/v2/internal/frontend/runtime/wrapper/wrapper.go new file mode 100644 index 000000000..94853bc7c --- /dev/null +++ b/v2/internal/frontend/runtime/wrapper/wrapper.go @@ -0,0 +1,6 @@ +package wrapper + +import "embed" + +//go:embed runtime.js runtime.d.ts package.json +var RuntimeWrapper embed.FS diff --git a/v2/internal/frontend/utils/urlValidator.go b/v2/internal/frontend/utils/urlValidator.go new file mode 100644 index 000000000..76ba216ce --- /dev/null +++ b/v2/internal/frontend/utils/urlValidator.go @@ -0,0 +1,58 @@ +package utils + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" +) + +func ValidateAndSanitizeURL(rawURL string) (string, error) { + // Check for null bytes (can cause truncation issues in some systems) + if strings.Contains(rawURL, "\x00") { + return "", errors.New("null bytes not allowed in URL") + } + + // Parse URL first - this handles most malformed URLs + parsedURL, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("invalid URL format: %v", err) + } + + scheme := strings.ToLower(parsedURL.Scheme) + + if scheme == "javascript" || scheme == "data" || scheme == "file" || scheme == "ftp" || scheme == "" { + return "", errors.New("scheme not allowed") + } + + // Ensure there's actually a host for http/https URLs + if (scheme == "http" || scheme == "https") && parsedURL.Host == "" { + return "", fmt.Errorf("missing host for %s URL", scheme) + } + + sanitizedURL := parsedURL.String() + + // Check for control characters that might cause issues + // (but allow legitimate URL characters like &, ;, etc.) + for i, r := range sanitizedURL { + // Block control characters except tab, but allow other printable chars + if r < 32 && r != 9 { // 9 is tab, which might be legitimate + return "", fmt.Errorf("control character at position %d not allowed", i) + } + } + + // Shell metacharacter check + shellDangerous := `[;\|` + "`" + `$\\<>*{}\[\]()~! \t\n\r]` + if matched, _ := regexp.MatchString(shellDangerous, sanitizedURL); matched { + return "", errors.New("shell metacharacters not allowed") + } + + // Unicode danger check + unicodeDangerous := "[\u0000-\u001F\u007F\u00A0\u1680\u2000-\u200F\u2028-\u202F\u205F\u2060\u3000\uFEFF]" + if matched, _ := regexp.MatchString(unicodeDangerous, sanitizedURL); matched { + return "", errors.New("unicode dangerous characters not allowed") + } + + return sanitizedURL, nil +} diff --git a/v2/internal/frontend/utils/urlValidator_test.go b/v2/internal/frontend/utils/urlValidator_test.go new file mode 100644 index 000000000..b385ccec1 --- /dev/null +++ b/v2/internal/frontend/utils/urlValidator_test.go @@ -0,0 +1,207 @@ +package utils_test + +import ( + "strings" + "testing" + + "github.com/wailsapp/wails/v2/internal/frontend/utils" +) + +// Test cases for ValidateAndOpenURL +func TestValidateURL(t *testing.T) { + testCases := []struct { + name string + url string + shouldErr bool + errMsg string + expected string + }{ + // Valid URLs + { + name: "valid https URL", + url: "https://www.example.com", + shouldErr: false, + expected: "https://www.example.com", + }, + { + name: "valid http URL", + url: "http://example.com", + shouldErr: false, + expected: "http://example.com", + }, + { + name: "URL with query parameters", + url: "https://example.com/search?q=cats&dogs", + shouldErr: false, + expected: "https://example.com/search?q=cats&dogs", + }, + { + name: "URL with port", + url: "https://example.com:8080/path", + shouldErr: false, + expected: "https://example.com:8080/path", + }, + { + name: "URL with fragment", + url: "https://example.com/page#section", + shouldErr: false, + expected: "https://example.com/page#section", + }, + { + name: "urlencode params", + url: "http://google.com/ ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat", + shouldErr: false, + expected: "http://google.com/%20----browser-subprocess-path=C:%5C%5CUsers%5C%5CPublic%5C%5Ctest.bat", + }, + + // Invalid schemes + { + name: "javascript scheme", + url: "javascript:alert('xss')", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "data scheme", + url: "data:text/html,", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "file scheme", + url: "file:///etc/passwd", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "ftp scheme", + url: "ftp://files.example.com/file.txt", + shouldErr: true, + errMsg: "scheme not allowed", + }, + + // Malformed URLs + { + name: "not a URL", + url: "not-a-url", + shouldErr: true, + errMsg: "scheme not allowed", // will have empty scheme + }, + { + name: "missing scheme", + url: "example.com", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "malformed URL", + url: "https://", + shouldErr: true, + errMsg: "missing host", + }, + { + name: "empty host", + url: "http:///path", + shouldErr: true, + errMsg: "missing host", + }, + + // Security issues + { + name: "null byte in URL", + url: "https://example.com\x00/hidden", + shouldErr: true, + errMsg: "null bytes not allowed", + }, + { + name: "control characters", + url: "https://example.com\n/path", + shouldErr: true, + errMsg: "control character", + }, + { + name: "carriage return", + url: "https://example.com\r/path", + shouldErr: true, + errMsg: "control character", + }, + { + name: "URL with tab character", + url: "https://example.com/path?q=hello\tworld", + shouldErr: true, + errMsg: "control character", + }, + { + name: "URL with path parameters", + url: "https://example.com/path;param=value", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with special characters in query", + url: "https://example.com/search?q=hello world&filter=price>100", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with special characters in query and params", + url: "https://example.com/search?q=hello ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with dollar sign in query", + url: "https://example.com/search?price=$100", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with parentheses", + url: "https://example.com/file(1).html", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with unicode", + url: "https://example.com/search?q=hello\u2001foo", + shouldErr: true, + errMsg: "unicode dangerous characters not allowed", + }, + + // Edge cases + { + name: "international domain", + url: "https://例え.テスト/path", + shouldErr: false, + expected: "https://%E4%BE%8B%E3%81%88.%E3%83%86%E3%82%B9%E3%83%88/path", + }, + { + name: "URL with pipe character", + url: "https://example.com/user/123|admin", + shouldErr: false, + expected: "https://example.com/user/123%7Cadmin", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // We'll test only the validation part to avoid actually opening URLs + sanitized, err := utils.ValidateAndSanitizeURL(tc.url) + + if tc.shouldErr { + if err == nil { + t.Errorf("expected error for URL %q, but got none", tc.url) + } else if tc.errMsg != "" && !strings.Contains(err.Error(), tc.errMsg) { + t.Errorf("expected error containing %q, got %q", tc.errMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error for URL %q, but got: %v", tc.url, err) + } + if sanitized != tc.expected { + t.Errorf("unexpected sanitized URL for %q: expected %q, got %q", tc.url, tc.expected, sanitized) + } + } + }) + } +} diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go new file mode 100644 index 000000000..5662c020c --- /dev/null +++ b/v2/internal/fs/fs.go @@ -0,0 +1,402 @@ +package fs + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "unsafe" + + "github.com/leaanthony/slicer" +) + +// RelativeToCwd returns an absolute path based on the cwd +// and the given relative path +func RelativeToCwd(relativePath string) (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + return filepath.Join(cwd, relativePath), nil +} + +// Mkdir will create the given directory +func Mkdir(dirname string) error { + return os.Mkdir(dirname, 0o755) +} + +// MkDirs creates the given nested directories. +// Returns error on failure +func MkDirs(fullPath string, mode ...os.FileMode) error { + var perms os.FileMode + perms = 0o755 + if len(mode) == 1 { + perms = mode[0] + } + return os.MkdirAll(fullPath, perms) +} + +// MoveFile attempts to move the source file to the target +// Target is a fully qualified path to a file *name*, not a +// directory +func MoveFile(source string, target string) error { + return os.Rename(source, target) +} + +// DeleteFile will delete the given file +func DeleteFile(filename string) error { + return os.Remove(filename) +} + +// CopyFile from source to target +func CopyFile(source string, target string) error { + s, err := os.Open(source) + if err != nil { + return err + } + defer s.Close() + d, err := os.Create(target) + if err != nil { + return err + } + if _, err := io.Copy(d, s); err != nil { + d.Close() + return err + } + return d.Close() +} + +// DirExists - Returns true if the given path resolves to a directory on the filesystem +func DirExists(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsDir() +} + +// FileExists returns a boolean value indicating whether +// the given file exists +func FileExists(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// RelativePath returns a qualified path created by joining the +// directory of the calling file and the given relative path. +// +// Example: RelativePath("..") in *this* file would give you '/path/to/wails2/v2/internal` +func RelativePath(relativepath string, optionalpaths ...string) string { + _, thisFile, _, _ := runtime.Caller(1) + localDir := filepath.Dir(thisFile) + + // If we have optional paths, join them to the relativepath + if len(optionalpaths) > 0 { + paths := []string{relativepath} + paths = append(paths, optionalpaths...) + relativepath = filepath.Join(paths...) + } + result, err := filepath.Abs(filepath.Join(localDir, relativepath)) + if err != nil { + // I'm allowing this for 1 reason only: It's fatal if the path + // supplied is wrong as it's only used internally in Wails. If we get + // that path wrong, we should know about it immediately. The other reason is + // that it cuts down a ton of unnecessary error handling. + panic(err) + } + return result +} + +// MustLoadString attempts to load a string and will abort with a fatal message if +// something goes wrong +func MustLoadString(filename string) string { + data, err := os.ReadFile(filename) + if err != nil { + fmt.Printf("FATAL: Unable to load file '%s': %s\n", filename, err.Error()) + os.Exit(1) + } + return *(*string)(unsafe.Pointer(&data)) +} + +// MD5File returns the md5sum of the given file +func MD5File(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() + + h := md5.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +// MustMD5File will call MD5File and abort the program on error +func MustMD5File(filename string) string { + result, err := MD5File(filename) + if err != nil { + println("FATAL: Unable to MD5Sum file:", err.Error()) + os.Exit(1) + } + return result +} + +// MustWriteString will attempt to write the given data to the given filename +// It will abort the program in the event of a failure +func MustWriteString(filename string, data string) { + err := os.WriteFile(filename, []byte(data), 0o755) + if err != nil { + fatal("Unable to write file", filename, ":", err.Error()) + os.Exit(1) + } +} + +// fatal will print the optional messages and die +func fatal(message ...string) { + if len(message) > 0 { + print("FATAL:") + for text := range message { + print(text) + } + } + os.Exit(1) +} + +// GetSubdirectories returns a list of subdirectories for the given root directory +func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) { + var result slicer.StringSlicer + + // Iterate root dir + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // If we have a directory, save it + if info.IsDir() { + result.Add(path) + } + return nil + }) + return &result, err +} + +func DirIsEmpty(dir string) (bool, error) { + // CREDIT: https://stackoverflow.com/a/30708914/8325411 + f, err := os.Open(dir) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err // Either not empty or error, suits both cases +} + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 +func CopyDir(src string, dst string) (err error) { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return fmt.Errorf("source is not a directory") + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return + } + if err == nil { + return fmt.Errorf("destination already exists") + } + + err = MkDirs(dst) + if err != nil { + return + } + + entries, err := os.ReadDir(src) + if err != nil { + return + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = CopyDir(srcPath, dstPath) + if err != nil { + return + } + } else { + // Skip symlinks. + if entry.Type()&os.ModeSymlink != 0 { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return +} + +// SetPermissions recursively sets file permissions on a directory +func SetPermissions(dir string, perm os.FileMode) error { + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + return os.Chmod(path, perm) + }) +} + +// CopyDirExtended recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. It ignores any files or +// directories that are given through the ignore parameter. +// Symlinks are ignored and skipped. +// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 +func CopyDirExtended(src string, dst string, ignore []string) (err error) { + ignoreList := slicer.String(ignore) + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return fmt.Errorf("source is not a directory") + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return + } + if err == nil { + return fmt.Errorf("destination already exists") + } + + err = MkDirs(dst) + if err != nil { + return + } + + entries, err := os.ReadDir(src) + if err != nil { + return + } + + for _, entry := range entries { + if ignoreList.Contains(entry.Name()) { + continue + } + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = CopyDir(srcPath, dstPath) + if err != nil { + return + } + } else { + // Skip symlinks. + if entry.Type()&os.ModeSymlink != 0 { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return +} + +func FindPathToFile(fsys fs.FS, file string) (string, error) { + stat, _ := fs.Stat(fsys, file) + if stat != nil { + return ".", nil + } + var indexFiles slicer.StringSlicer + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(path, file) { + indexFiles.Add(path) + } + return nil + }) + if err != nil { + return "", err + } + + if indexFiles.Length() > 1 { + selected := indexFiles.AsSlice()[0] + for _, f := range indexFiles.AsSlice() { + if len(f) < len(selected) { + selected = f + } + } + path, _ := filepath.Split(selected) + return path, nil + } + if indexFiles.Length() > 0 { + path, _ := filepath.Split(indexFiles.AsSlice()[0]) + return path, nil + } + return "", fmt.Errorf("%s: %w", file, os.ErrNotExist) +} + +// FindFileInParents searches for a file in the current directory and all parent directories. +// Returns the absolute path to the file if found, otherwise an empty string +func FindFileInParents(path string, filename string) string { + // Check for bad paths + if _, err := os.Stat(path); err != nil { + return "" + } + + var pathToFile string + for { + pathToFile = filepath.Join(path, filename) + if _, err := os.Stat(pathToFile); err == nil { + break + } + parent := filepath.Dir(path) + if parent == path { + return "" + } + path = parent + } + return pathToFile +} diff --git a/v2/internal/fs/fs_test.go b/v2/internal/fs/fs_test.go new file mode 100644 index 000000000..efc4929e6 --- /dev/null +++ b/v2/internal/fs/fs_test.go @@ -0,0 +1,90 @@ +package fs + +import ( + "github.com/samber/lo" + "os" + "path/filepath" + "testing" + + "github.com/matryer/is" +) + +func TestRelativePath(t *testing.T) { + + i := is.New(t) + + cwd, err := os.Getwd() + i.Equal(err, nil) + + // Check current directory + actual := RelativePath(".") + i.Equal(actual, cwd) + + // Check 2 parameters + actual = RelativePath("..", "fs") + i.Equal(actual, cwd) + + // Check 3 parameters including filename + actual = RelativePath("..", "fs", "fs.go") + expected := filepath.Join(cwd, "fs.go") + i.Equal(actual, expected) + +} + +func Test_FindFileInParents(t *testing.T) { + tests := []struct { + name string + setup func() (startDir string, configDir string) + wantErr bool + }{ + { + name: "should error when no wails.json file is found in local or parent dirs", + setup: func() (string, string) { + tempDir := os.TempDir() + testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath")) + _ = os.MkdirAll(testDir, 0755) + return testDir, "" + }, + wantErr: true, + }, + { + name: "should find wails.json in local path", + setup: func() (string, string) { + tempDir := os.TempDir() + testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath")) + _ = os.MkdirAll(testDir, 0755) + configFile := filepath.Join(testDir, "wails.json") + _ = os.WriteFile(configFile, []byte("{}"), 0755) + return testDir, configFile + }, + wantErr: false, + }, + { + name: "should find wails.json in parent path", + setup: func() (string, string) { + tempDir := os.TempDir() + testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath")) + _ = os.MkdirAll(testDir, 0755) + parentDir := filepath.Dir(testDir) + configFile := filepath.Join(parentDir, "wails.json") + _ = os.WriteFile(configFile, []byte("{}"), 0755) + return testDir, configFile + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, expectedPath := tt.setup() + defer func() { + if expectedPath != "" { + _ = os.Remove(expectedPath) + } + }() + got := FindFileInParents(path, "wails.json") + if got != expectedPath { + t.Errorf("FindFileInParents() got = %v, want %v", got, expectedPath) + } + }) + } +} diff --git a/v2/internal/github/github.go b/v2/internal/github/github.go new file mode 100644 index 000000000..2aa5e1432 --- /dev/null +++ b/v2/internal/github/github.go @@ -0,0 +1,147 @@ +package github + +import ( + "encoding/json" + "fmt" + "github.com/charmbracelet/glamour/styles" + "io" + "net/http" + "net/url" + "runtime" + "sort" + "strings" + + "github.com/charmbracelet/glamour" +) + +func GetReleaseNotes(tagVersion string, noColour bool) string { + resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/releases/tags/" + url.PathEscape(tagVersion)) + if err != nil { + return "Unable to retrieve release notes. Please check your network connection" + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "Unable to retrieve release notes. Please check your network connection" + } + + data := map[string]interface{}{} + err = json.Unmarshal(body, &data) + if err != nil { + return "Unable to retrieve release notes. Please check your network connection" + } + + if data["body"] == nil { + return "No release notes found" + } + + result := "# Release Notes for " + tagVersion + "\n" + data["body"].(string) + var renderer *glamour.TermRenderer + + var termRendererOpts []glamour.TermRendererOption + + if runtime.GOOS == "windows" || noColour { + termRendererOpts = append(termRendererOpts, glamour.WithStyles(styles.NoTTYStyleConfig)) + } else { + termRendererOpts = append(termRendererOpts, glamour.WithAutoStyle()) + } + + renderer, err = glamour.NewTermRenderer(termRendererOpts...) + if err != nil { + return result + } + result, err = renderer.Render(result) + if err != nil { + return err.Error() + } + return result +} + +// GetVersionTags gets the list of tags on the Wails repo +// It returns a list of sorted tags in descending order +func GetVersionTags() ([]*SemanticVersion, error) { + result := []*SemanticVersion{} + var err error + + resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags") + if err != nil { + return result, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + data := []map[string]interface{}{} + err = json.Unmarshal(body, &data) + if err != nil { + return result, err + } + + // Convert tag data to Version structs + for _, tag := range data { + version := tag["name"].(string) + if !strings.HasPrefix(version, "v2") { + continue + } + semver, err := NewSemanticVersion(version) + if err != nil { + return result, err + } + result = append(result, semver) + } + + // Reverse Sort + sort.Sort(sort.Reverse(SemverCollection(result))) + + return result, err +} + +// GetLatestStableRelease gets the latest stable release on GitHub +func GetLatestStableRelease() (result *SemanticVersion, err error) { + tags, err := GetVersionTags() + if err != nil { + return nil, err + } + + for _, tag := range tags { + if tag.IsRelease() { + return tag, nil + } + } + + return nil, fmt.Errorf("no release tag found") +} + +// GetLatestPreRelease gets the latest prerelease on GitHub +func GetLatestPreRelease() (result *SemanticVersion, err error) { + tags, err := GetVersionTags() + if err != nil { + return nil, err + } + + for _, tag := range tags { + if tag.IsPreRelease() { + return tag, nil + } + } + + return nil, fmt.Errorf("no prerelease tag found") +} + +// IsValidTag returns true if the given string is a valid tag +func IsValidTag(tagVersion string) (bool, error) { + if tagVersion[0] == 'v' { + tagVersion = tagVersion[1:] + } + tags, err := GetVersionTags() + if err != nil { + return false, err + } + + for _, tag := range tags { + if tag.String() == tagVersion { + return true, nil + } + } + return false, nil +} diff --git a/v2/internal/github/semver.go b/v2/internal/github/semver.go new file mode 100644 index 000000000..1cf5907fa --- /dev/null +++ b/v2/internal/github/semver.go @@ -0,0 +1,106 @@ +package github + +import ( + "fmt" + + "github.com/Masterminds/semver" +) + +// SemanticVersion is a struct containing a semantic version +type SemanticVersion struct { + Version *semver.Version +} + +// NewSemanticVersion creates a new SemanticVersion object with the given version string +func NewSemanticVersion(version string) (*SemanticVersion, error) { + semverVersion, err := semver.NewVersion(version) + if err != nil { + return nil, err + } + return &SemanticVersion{ + Version: semverVersion, + }, nil +} + +// IsRelease returns true if it's a release version +func (s *SemanticVersion) IsRelease() bool { + // Limit to v2 + if s.Version.Major() != 2 { + return false + } + return len(s.Version.Prerelease()) == 0 && len(s.Version.Metadata()) == 0 +} + +// IsPreRelease returns true if it's a prerelease version +func (s *SemanticVersion) IsPreRelease() bool { + // Limit to v1 + if s.Version.Major() != 2 { + return false + } + return len(s.Version.Prerelease()) > 0 +} + +func (s *SemanticVersion) String() string { + return s.Version.String() +} + +// IsGreaterThan returns true if this version is greater than the given version +func (s *SemanticVersion) IsGreaterThan(version *SemanticVersion) (bool, error) { + // Set up new constraint + constraint, err := semver.NewConstraint("> " + version.Version.String()) + if err != nil { + return false, err + } + + // Check if the desired one is greater than the requested on + success, msgs := constraint.Validate(s.Version) + if !success { + return false, msgs[0] + } + return true, nil +} + +// IsGreaterThanOrEqual returns true if this version is greater than or equal the given version +func (s *SemanticVersion) IsGreaterThanOrEqual(version *SemanticVersion) (bool, error) { + // Set up new constraint + constraint, err := semver.NewConstraint(">= " + version.Version.String()) + if err != nil { + return false, err + } + + // Check if the desired one is greater than the requested on + success, msgs := constraint.Validate(s.Version) + if !success { + return false, msgs[0] + } + return true, nil +} + +// MainVersion returns the main version of any version+prerelease+metadata +// EG: MainVersion("1.2.3-pre") => "1.2.3" +func (s *SemanticVersion) MainVersion() *SemanticVersion { + mainVersion := fmt.Sprintf("%d.%d.%d", s.Version.Major(), s.Version.Minor(), s.Version.Patch()) + result, _ := NewSemanticVersion(mainVersion) + return result +} + +// SemverCollection is a collection of SemanticVersion objects +type SemverCollection []*SemanticVersion + +// Len returns the length of a collection. The number of Version instances +// on the slice. +func (c SemverCollection) Len() int { + return len(c) +} + +// Less is needed for the sort interface to compare two Version objects on the +// slice. If checks if one is less than the other. +func (c SemverCollection) Less(i, j int) bool { + return c[i].Version.LessThan(c[j].Version) +} + +// Swap is needed for the sort interface to replace the Version objects +// at two different positions in the slice. +func (c SemverCollection) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} diff --git a/v2/internal/github/semver_test.go b/v2/internal/github/semver_test.go new file mode 100644 index 000000000..f748a57a0 --- /dev/null +++ b/v2/internal/github/semver_test.go @@ -0,0 +1,43 @@ +package github + +import ( + "github.com/matryer/is" + "testing" +) + +func TestSemanticVersion_IsGreaterThan(t *testing.T) { + is2 := is.New(t) + + alpha1, err := NewSemanticVersion("v2.0.0-alpha.1") + is2.NoErr(err) + + beta1, err := NewSemanticVersion("v2.0.0-beta.1") + is2.NoErr(err) + + v2, err := NewSemanticVersion("v2.0.0") + is2.NoErr(err) + + is2.True(alpha1.IsPreRelease()) + is2.True(beta1.IsPreRelease()) + is2.True(!v2.IsPreRelease()) + is2.True(v2.IsRelease()) + + result, err := beta1.IsGreaterThan(alpha1) + is2.NoErr(err) + is2.True(result) + + result, err = v2.IsGreaterThan(beta1) + is2.NoErr(err) + is2.True(result) + + beta44, err := NewSemanticVersion("v2.0.0-beta.44.2") + is2.NoErr(err) + + rc1, err := NewSemanticVersion("v2.0.0-rc.1") + is2.NoErr(err) + + result, err = rc1.IsGreaterThan(beta44) + is2.NoErr(err) + is2.True(result) + +} diff --git a/licenses/github.com/jackmordaunt/icns/LICENSE b/v2/internal/go-common-file-dialog/LICENSE similarity index 96% rename from licenses/github.com/jackmordaunt/icns/LICENSE rename to v2/internal/go-common-file-dialog/LICENSE index 787939ec2..508b6978e 100644 --- a/licenses/github.com/jackmordaunt/icns/LICENSE +++ b/v2/internal/go-common-file-dialog/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Jack Mordaunt +Copyright (c) 2019 Harry Phillips Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/v2/internal/go-common-file-dialog/README.md b/v2/internal/go-common-file-dialog/README.md new file mode 100644 index 000000000..1cb5902d1 --- /dev/null +++ b/v2/internal/go-common-file-dialog/README.md @@ -0,0 +1,31 @@ +# Common File Dialog bindings for Golang + +[Project Home](https://github.com/harry1453/go-common-file-dialog) + +This library contains bindings for Windows Vista and +newer's [Common File Dialogs](https://docs.microsoft.com/en-us/windows/win32/shell/common-file-dialog), which is the +standard system dialog for selecting files or folders to open or save. + +The Common File Dialogs have to be accessed via +the [COM Interface](https://en.wikipedia.org/wiki/Component_Object_Model), normally via C++ or via bindings (like in C#) +. + +This library contains bindings for Golang. **It does not require CGO**, and contains empty stubs for non-windows +platforms (so is safe to compile and run on platforms other than windows, but will just return errors at runtime). + +This can be very useful if you want to quickly get a file selector in your Golang application. The `cfdutil` package +contains utility functions with a single call to open and configure a dialog, and then get the result from it. Examples +for this are in [`_examples/usingutil`](_examples/usingutil). Or, if you want finer control over the dialog's operation, +you can use the base package. Examples for this are in [`_examples/notusingutil`](_examples/notusingutil). + +This library is available under the MIT license. + +Currently supported features: + +* Open File Dialog (to open a single file) +* Open Multiple Files Dialog (to open multiple files) +* Open Folder Dialog +* Save File Dialog +* Dialog "roles" to allow Windows to remember different "last locations" for different types of dialog +* Set dialog Title, Default Folder and Initial Folder +* Set dialog File Filters diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go new file mode 100644 index 000000000..58e97aa4e --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go @@ -0,0 +1,72 @@ +// Cross-platform. + +// Common File Dialogs +package cfd + +type Dialog interface { + // Show the dialog to the user. + // Blocks until the user has closed the dialog. + Show() error + // Sets the dialog's parent window. Use 0 to set the dialog to have no parent window. + SetParentWindowHandle(hwnd uintptr) + // Show the dialog to the user. + // Blocks until the user has closed the dialog and returns their selection. + // Returns an error if the user cancelled the dialog. + // Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead. + ShowAndGetResult() (string, error) + // Sets the title of the dialog window. + SetTitle(title string) error + // Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the + // OS will use to differentiate it from dialogs that are intended for other purposes. + // This means that, for example, a dialog with role "Import" will have a different + // previous location that it will open to than a dialog with role "Open". Can be any string. + SetRole(role string) error + // Sets the folder used as a default if there is not a recently used folder value available + SetDefaultFolder(defaultFolder string) error + // Sets the folder that the dialog always opens to. + // If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder. + SetFolder(folder string) error + // Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt" + // Do not use for the Open Multiple Files dialog. Use GetResults instead. + GetResult() (string, error) + // Sets the file name, I.E. the contents of the file name text box. + // For Select Folder Dialog, sets folder name. + SetFileName(fileName string) error + // Release the resources allocated to this Dialog. + // Should be called when the dialog is finished with. + Release() error +} + +type FileDialog interface { + Dialog + // Set the list of file filters that the user can select. + SetFileFilters(fileFilter []FileFilter) error + // Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called. + SetSelectedFileFilterIndex(index uint) error + // Sets the default extension applied when a user does not provide one as part of the file name. + // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. + // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. + // For Save File Dialog, this extension will be used whenever a user does not specify an extension. + SetDefaultExtension(defaultExtension string) error +} + +type OpenFileDialog interface { + FileDialog +} + +type OpenMultipleFilesDialog interface { + FileDialog + // Show the dialog to the user. + // Blocks until the user has closed the dialog and returns the selected files. + ShowAndGetResults() ([]string, error) + // Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt" + GetResults() ([]string, error) +} + +type SelectFolderDialog interface { + Dialog +} + +type SaveFileDialog interface { // TODO Properties + FileDialog +} diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go new file mode 100644 index 000000000..3ab969850 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go @@ -0,0 +1,28 @@ +//go:build !windows +// +build !windows + +package cfd + +import "fmt" + +var unsupportedError = fmt.Errorf("common file dialogs are only available on windows") + +// TODO doc +func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { + return nil, unsupportedError +} diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go new file mode 100644 index 000000000..69f46118e --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go @@ -0,0 +1,79 @@ +//go:build windows +// +build windows + +package cfd + +import "github.com/go-ole/go-ole" + +func initialize() { + // Swallow error + _ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE) +} + +// TODO doc +func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + err = openDialog.setIsMultiselect(true) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + err = openDialog.setPickFolders(true) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { + initialize() + + saveDialog, err := newIFileSaveDialog() + if err != nil { + return nil, err + } + err = config.apply(saveDialog) + if err != nil { + return nil, err + } + return saveDialog, nil +} diff --git a/v2/internal/go-common-file-dialog/cfd/DialogConfig.go b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go new file mode 100644 index 000000000..9e06fb503 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go @@ -0,0 +1,141 @@ +// Cross-platform. + +package cfd + +import ( + "fmt" + "os" + "reflect" +) + +type FileFilter struct { + // The display name of the filter (That is shown to the user) + DisplayName string + // The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc. + Pattern string +} + +// Never obfuscate the FileFilter type. +var _ = reflect.TypeOf(FileFilter{}) + +type DialogConfig struct { + // The title of the dialog + Title string + // The role of the dialog. This is used to derive the dialog's GUID, which the + // OS will use to differentiate it from dialogs that are intended for other purposes. + // This means that, for example, a dialog with role "Import" will have a different + // previous location that it will open to than a dialog with role "Open". Can be any string. + Role string + // The default folder - the folder that is used the first time the user opens it + // (after the first time their last used location is used). + DefaultFolder string + // The initial folder - the folder that the dialog always opens to if not empty. + // If this is not empty, it will override the "default folder" behaviour and + // the dialog will always open to this folder. + Folder string + // The file filters that restrict which types of files the dialog is able to choose. + // Ignored by Select Folder Dialog. + FileFilters []FileFilter + // Sets the initially selected file filter. This is an index of FileFilters. + // Ignored by Select Folder Dialog. + SelectedFileFilterIndex uint + // The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog. + // For the Select Folder Dialog, this sets the initial folder name. + FileName string + // The default extension applied when a user does not provide one as part of the file name. + // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. + // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. + // For Save File Dialog, this extension will be used whenever a user does not specify an extension. + // Ignored by Select Folder Dialog. + DefaultExtension string + // ParentWindowHandle is the handle (HWND) to the parent window of the dialog. + // If left as 0 / nil, the dialog will have no parent window. + ParentWindowHandle uintptr +} + +var defaultFilters = []FileFilter{ + { + DisplayName: "All Files (*.*)", + Pattern: "*.*", + }, +} + +func (config *DialogConfig) apply(dialog Dialog) (err error) { + if config.Title != "" { + err = dialog.SetTitle(config.Title) + if err != nil { + return + } + } + + if config.Role != "" { + err = dialog.SetRole(config.Role) + if err != nil { + return + } + } + + if config.Folder != "" { + _, err = os.Stat(config.Folder) + if err != nil { + return + } + err = dialog.SetFolder(config.Folder) + if err != nil { + return + } + } + + if config.DefaultFolder != "" { + _, err = os.Stat(config.DefaultFolder) + if err != nil { + return + } + err = dialog.SetDefaultFolder(config.DefaultFolder) + if err != nil { + return + } + } + + if config.FileName != "" { + err = dialog.SetFileName(config.FileName) + if err != nil { + return + } + } + + dialog.SetParentWindowHandle(config.ParentWindowHandle) + + if dialog, ok := dialog.(FileDialog); ok { + var fileFilters []FileFilter + if config.FileFilters != nil && len(config.FileFilters) > 0 { + fileFilters = config.FileFilters + } else { + fileFilters = defaultFilters + } + err = dialog.SetFileFilters(fileFilters) + if err != nil { + return + } + + if config.SelectedFileFilterIndex != 0 { + if config.SelectedFileFilterIndex > uint(len(fileFilters)) { + err = fmt.Errorf("selected file filter index out of range") + return + } + err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex) + if err != nil { + return + } + } + + if config.DefaultExtension != "" { + err = dialog.SetDefaultExtension(config.DefaultExtension) + if err != nil { + return + } + } + } + + return +} diff --git a/v2/internal/go-common-file-dialog/cfd/errors.go b/v2/internal/go-common-file-dialog/cfd/errors.go new file mode 100644 index 000000000..4ca3300b9 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/errors.go @@ -0,0 +1,9 @@ +package cfd + +import "errors" + +var ( + ErrCancelled = errors.New("cancelled by user") + ErrInvalidGUID = errors.New("guid cannot be nil") + ErrEmptyFilters = errors.New("must specify at least one filter") +) diff --git a/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go new file mode 100644 index 000000000..b1be23fcf --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go @@ -0,0 +1,200 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "github.com/google/uuid" + "syscall" + "unsafe" +) + +var ( + fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}") + fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}") +) + +type iFileOpenDialog struct { + vtbl *iFileOpenDialogVtbl + parentWindowHandle uintptr +} + +type iFileOpenDialogVtbl struct { + iFileDialogVtbl + + GetResults uintptr // func (ppenum **IShellItemArray) HRESULT + GetSelectedItems uintptr +} + +func newIFileOpenDialog() (*iFileOpenDialog, error) { + if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil { + return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil + } else { + return nil, err + } +} + +func (fileOpenDialog *iFileOpenDialog) Show() error { + return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle) +} + +func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) { + fileOpenDialog.parentWindowHandle = hwnd +} + +func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return "", err + } + if isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use ShowAndGetResults for open multiple files dialog") + } + if err := fileOpenDialog.Show(); err != nil { + return "", err + } + return fileOpenDialog.GetResult() +} + +func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return nil, err + } + if !isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use ShowAndGetResult for open single file dialog") + } + if err := fileOpenDialog.Show(); err != nil { + return nil, err + } + return fileOpenDialog.GetResults() +} + +func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error { + return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title) +} + +func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return "", err + } + if isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use GetResults for open multiple files dialog") + } + return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) Release() error { + return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error { + return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) +} + +func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error { + return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) +} + +func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error { + return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter) +} + +func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error { + return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), StringToUUID(role)) +} + +// This should only be callable when the user asks for a multi select because +// otherwise they will be given the Dialog interface which does not expose this function. +func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return nil, err + } + if !isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use GetResult for open single file dialog") + } + return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error { + return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension) +} + +func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error { + return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName) +} + +func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error { + return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index) +} + +func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error { + const FosPickfolders = 0x20 + if pickFolders { + return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) + } else { + return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) + } +} + +const FosAllowMultiselect = 0x200 + +func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) { + options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog)) + if err != nil { + return false, err + } + return options&FosAllowMultiselect != 0, nil +} + +func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error { + if isMultiselect { + return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) + } else { + return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) + } +} + +func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) { + var shellItemArray *iShellItemArray + ret, _, _ := syscall.SyscallN(vtbl.GetResults, + uintptr(objPtr), + uintptr(unsafe.Pointer(&shellItemArray)), + 0) + return shellItemArray, hresultToError(ret) +} + +func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) { + shellItemArray, err := vtbl.getResults(objPtr) + if err != nil { + return nil, err + } + if shellItemArray == nil { + return nil, ErrCancelled + } + defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray)) + count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray)) + if err != nil { + return nil, err + } + var results []string + for i := uintptr(0); i < count; i++ { + newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i) + if err != nil { + return nil, err + } + results = append(results, newItem) + } + return results, nil +} + +func StringToUUID(str string) *ole.GUID { + return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String()) +} diff --git a/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go new file mode 100644 index 000000000..ddee7b246 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go @@ -0,0 +1,92 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "unsafe" +) + +var ( + saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}") + saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}") +) + +type iFileSaveDialog struct { + vtbl *iFileSaveDialogVtbl + parentWindowHandle uintptr +} + +type iFileSaveDialogVtbl struct { + iFileDialogVtbl + + SetSaveAsItem uintptr + SetProperties uintptr + SetCollectedProperties uintptr + GetProperties uintptr + ApplyProperties uintptr +} + +func newIFileSaveDialog() (*iFileSaveDialog, error) { + if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil { + return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil + } else { + return nil, err + } +} + +func (fileSaveDialog *iFileSaveDialog) Show() error { + return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle) +} + +func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) { + fileSaveDialog.parentWindowHandle = hwnd +} + +func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) { + if err := fileSaveDialog.Show(); err != nil { + return "", err + } + return fileSaveDialog.GetResult() +} + +func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error { + return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title) +} + +func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) { + return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog)) +} + +func (fileSaveDialog *iFileSaveDialog) Release() error { + return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog)) +} + +func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error { + return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) +} + +func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error { + return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) +} + +func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error { + return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter) +} + +func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error { + return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), StringToUUID(role)) +} + +func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error { + return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension) +} + +func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error { + return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName) +} + +func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error { + return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index) +} diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItem.go b/v2/internal/go-common-file-dialog/cfd/iShellItem.go new file mode 100644 index 000000000..080115345 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iShellItem.go @@ -0,0 +1,56 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "syscall" + "unsafe" +) + +var ( + procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName") + iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe") +) + +type iShellItem struct { + vtbl *iShellItemVtbl +} + +type iShellItemVtbl struct { + iUnknownVtbl + BindToHandler uintptr + GetParent uintptr + GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT + GetAttributes uintptr + Compare uintptr +} + +func newIShellItem(path string) (*iShellItem, error) { + var shellItem *iShellItem + pathPtr := ole.SysAllocString(path) + defer func(v *int16) { + _ = ole.SysFreeString(v) + }(pathPtr) + + ret, _, _ := procSHCreateItemFromParsingName.Call( + uintptr(unsafe.Pointer(pathPtr)), + 0, + uintptr(unsafe.Pointer(iidShellItem)), + uintptr(unsafe.Pointer(&shellItem))) + return shellItem, hresultToError(ret) +} + +func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) { + var ptr *uint16 + ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName, + uintptr(objPtr), + 0x80058000, // SIGDN_FILESYSPATH, + uintptr(unsafe.Pointer(&ptr))) + if err := hresultToError(ret); err != nil { + return "", err + } + defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr))) + return ole.LpOleStrToString(ptr), nil +} diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go new file mode 100644 index 000000000..c548160d1 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go @@ -0,0 +1,64 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "syscall" + "unsafe" +) + +const ( + iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}" +) + +var ( + iidShellItemArray *ole.GUID +) + +func init() { + iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID) +} + +type iShellItemArray struct { + vtbl *iShellItemArrayVtbl +} + +type iShellItemArrayVtbl struct { + iUnknownVtbl + BindToHandler uintptr + GetPropertyStore uintptr + GetPropertyDescriptionList uintptr + GetAttributes uintptr + GetCount uintptr // func (pdwNumItems *DWORD) HRESULT + GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT + EnumItems uintptr +} + +func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) { + var count uintptr + ret, _, _ := syscall.SyscallN(vtbl.GetCount, + uintptr(objPtr), + uintptr(unsafe.Pointer(&count))) + if err := hresultToError(ret); err != nil { + return 0, err + } + return count, nil +} + +func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) { + var shellItem *iShellItem + ret, _, _ := syscall.SyscallN(vtbl.GetItemAt, + uintptr(objPtr), + index, + uintptr(unsafe.Pointer(&shellItem))) + if err := hresultToError(ret); err != nil { + return "", err + } + if shellItem == nil { + return "", ErrCancelled + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) +} diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommon.go b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go new file mode 100644 index 000000000..21015c27c --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go @@ -0,0 +1,48 @@ +//go:build windows +// +build windows + +package cfd + +type comDlgFilterSpec struct { + pszName *int16 + pszSpec *int16 +} + +type iUnknownVtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type iModalWindowVtbl struct { + iUnknownVtbl + Show uintptr // func (hwndOwner HWND) HRESULT +} + +type iFileDialogVtbl struct { + iModalWindowVtbl + SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT + SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT + GetFileTypeIndex uintptr + Advise uintptr + Unadvise uintptr + SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT + GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT + SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT + SetFolder uintptr // func (psi *IShellItem) HRESULT + GetFolder uintptr + GetCurrentSelection uintptr + SetFileName uintptr // func (pszName LPCWSTR) HRESULT + GetFileName uintptr + SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT + SetOkButtonLabel uintptr + SetFileNameLabel uintptr + GetResult uintptr // func (ppsi **IShellItem) HRESULT + AddPlace uintptr + SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT + // This can only be used from a callback. + Close uintptr + SetClientGuid uintptr // func (guid REFGUID) HRESULT + ClearClientData uintptr + SetFilter uintptr +} diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go new file mode 100644 index 000000000..581a7b25c --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go @@ -0,0 +1,224 @@ +//go:build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "strings" + "syscall" + "unsafe" +) + +func hresultToError(hr uintptr) error { + if hr < 0 { + return ole.NewError(hr) + } + return nil +} + +func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error { + ret, _, _ := syscall.SyscallN(vtbl.Release, + uintptr(objPtr), + 0) + return hresultToError(ret) +} + +func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error { + ret, _, _ := syscall.SyscallN(vtbl.Show, + uintptr(objPtr), + hwnd) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error { + cFileTypes := len(filters) + if cFileTypes < 0 { + return ErrEmptyFilters + } + comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes) + for i := 0; i < cFileTypes; i++ { + filter := &filters[i] + comDlgFilterSpecs[i] = comDlgFilterSpec{ + pszName: ole.SysAllocString(filter.DisplayName), + pszSpec: ole.SysAllocString(filter.Pattern), + } + } + + // Ensure memory is freed after use + defer func() { + for _, spec := range comDlgFilterSpecs { + ole.SysFreeString(spec.pszName) + ole.SysFreeString(spec.pszSpec) + } + }() + + ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes, + uintptr(objPtr), + uintptr(cFileTypes), + uintptr(unsafe.Pointer(&comDlgFilterSpecs[0]))) + return hresultToError(ret) +} + +// Options are: +// FOS_OVERWRITEPROMPT = 0x2, +// FOS_STRICTFILETYPES = 0x4, +// FOS_NOCHANGEDIR = 0x8, +// FOS_PICKFOLDERS = 0x20, +// FOS_FORCEFILESYSTEM = 0x40, +// FOS_ALLNONSTORAGEITEMS = 0x80, +// FOS_NOVALIDATE = 0x100, +// FOS_ALLOWMULTISELECT = 0x200, +// FOS_PATHMUSTEXIST = 0x800, +// FOS_FILEMUSTEXIST = 0x1000, +// FOS_CREATEPROMPT = 0x2000, +// FOS_SHAREAWARE = 0x4000, +// FOS_NOREADONLYRETURN = 0x8000, +// FOS_NOTESTFILECREATE = 0x10000, +// FOS_HIDEMRUPLACES = 0x20000, +// FOS_HIDEPINNEDPLACES = 0x40000, +// FOS_NODEREFERENCELINKS = 0x100000, +// FOS_OKBUTTONNEEDSINTERACTION = 0x200000, +// FOS_DONTADDTORECENT = 0x2000000, +// FOS_FORCESHOWHIDDEN = 0x10000000, +// FOS_DEFAULTNOMINIMODE = 0x20000000, +// FOS_FORCEPREVIEWPANEON = 0x40000000, +// FOS_SUPPORTSTREAMABLEITEMS = 0x80000000 +func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error { + ret, _, _ := syscall.SyscallN(vtbl.SetOptions, + uintptr(objPtr), + uintptr(options)) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) { + var options uint32 + ret, _, _ := syscall.SyscallN(vtbl.GetOptions, + uintptr(objPtr), + uintptr(unsafe.Pointer(&options))) + return options, hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error { + if options, err := vtbl.getOptions(objPtr); err == nil { + return vtbl.setOptions(objPtr, options|option) + } else { + return err + } +} + +func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error { + if options, err := vtbl.getOptions(objPtr); err == nil { + return vtbl.setOptions(objPtr, options&^option) + } else { + return err + } +} + +func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error { + shellItem, err := newIShellItem(path) + if err != nil { + return err + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder, + uintptr(objPtr), + uintptr(unsafe.Pointer(shellItem))) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error { + shellItem, err := newIShellItem(path) + if err != nil { + return err + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + ret, _, _ := syscall.SyscallN(vtbl.SetFolder, + uintptr(objPtr), + uintptr(unsafe.Pointer(shellItem))) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error { + titlePtr := ole.SysAllocString(title) + defer ole.SysFreeString(titlePtr) // Ensure the string is freed + ret, _, _ := syscall.SyscallN(vtbl.SetTitle, + uintptr(objPtr), + uintptr(unsafe.Pointer(titlePtr))) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error { + ret, _, _ := syscall.SyscallN(vtbl.Close, + uintptr(objPtr)) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) { + var shellItem *iShellItem + ret, _, _ := syscall.SyscallN(vtbl.GetResult, + uintptr(objPtr), + uintptr(unsafe.Pointer(&shellItem))) + return shellItem, hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) { + shellItem, err := vtbl.getResult(objPtr) + if err != nil { + return "", err + } + if shellItem == nil { + return "", ErrCancelled + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) +} + +func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error { + // Ensure the GUID is not nil + if guid == nil { + return ErrInvalidGUID + } + + // Call the SetClientGuid method + ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid, + uintptr(objPtr), + uintptr(unsafe.Pointer(guid))) + + // Convert the HRESULT to a Go error + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error { + // Ensure the string is not empty before accessing the first character + if len(defaultExtension) > 0 && defaultExtension[0] == '.' { + defaultExtension = strings.TrimPrefix(defaultExtension, ".") + } + + // Allocate memory for the default extension string + defaultExtensionPtr := ole.SysAllocString(defaultExtension) + defer ole.SysFreeString(defaultExtensionPtr) // Ensure the string is freed + + // Call the SetDefaultExtension method + ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension, + uintptr(objPtr), + uintptr(unsafe.Pointer(defaultExtensionPtr))) + + // Convert the HRESULT to a Go error + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error { + fileNamePtr := ole.SysAllocString(fileName) + defer ole.SysFreeString(fileNamePtr) // Ensure the string is freed + ret, _, _ := syscall.SyscallN(vtbl.SetFileName, + uintptr(objPtr), + uintptr(unsafe.Pointer(fileNamePtr))) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error { + ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex, + uintptr(objPtr), + uintptr(index+1)) // SetFileTypeIndex counts from 1 + return hresultToError(ret) +} diff --git a/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go new file mode 100644 index 000000000..bde52d743 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go @@ -0,0 +1,45 @@ +package cfdutil + +import ( + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" +) + +// TODO doc +func ShowOpenFileDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewOpenFileDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} + +// TODO doc +func ShowOpenMultipleFilesDialog(config cfd.DialogConfig) ([]string, error) { + dialog, err := cfd.NewOpenMultipleFilesDialog(config) + if err != nil { + return nil, err + } + defer dialog.Release() + return dialog.ShowAndGetResults() +} + +// TODO doc +func ShowPickFolderDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewSelectFolderDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} + +// TODO doc +func ShowSaveFileDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewSaveFileDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} diff --git a/v2/internal/go-common-file-dialog/util/util.go b/v2/internal/go-common-file-dialog/util/util.go new file mode 100644 index 000000000..723fbedc0 --- /dev/null +++ b/v2/internal/go-common-file-dialog/util/util.go @@ -0,0 +1,10 @@ +package util + +import ( + "github.com/go-ole/go-ole" + "github.com/google/uuid" +) + +func StringToUUID(str string) *ole.GUID { + return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String()) +} diff --git a/v2/internal/go-common-file-dialog/util/util_test.go b/v2/internal/go-common-file-dialog/util/util_test.go new file mode 100644 index 000000000..2e8ffeb05 --- /dev/null +++ b/v2/internal/go-common-file-dialog/util/util_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "github.com/go-ole/go-ole" + "testing" +) + +func TestStringToUUID(t *testing.T) { + generated := *StringToUUID("TestTestTest") + expected := *ole.NewGUID("7933985F-2C87-5A5B-A26E-5D0326829AC2") + if generated != expected { + t.Errorf("not equal. expected %s, found %s", expected.String(), generated.String()) + } +} diff --git a/v2/internal/gomod/gomod.go b/v2/internal/gomod/gomod.go new file mode 100644 index 000000000..c38e60f0b --- /dev/null +++ b/v2/internal/gomod/gomod.go @@ -0,0 +1,114 @@ +package gomod + +import ( + "fmt" + + "github.com/Masterminds/semver" + "golang.org/x/mod/modfile" +) + +func GetWailsVersionFromModFile(goModText []byte) (*semver.Version, error) { + file, err := modfile.Parse("", goModText, nil) + if err != nil { + return nil, err + } + + for _, req := range file.Require { + if req.Syntax == nil { + continue + } + tokenPosition := 0 + if !req.Syntax.InBlock { + tokenPosition = 1 + } + if req.Syntax.Token[tokenPosition] == "github.com/wailsapp/wails/v2" { + version := req.Syntax.Token[tokenPosition+1] + return semver.NewVersion(version) + } + } + + return nil, nil +} + +func GoModOutOfSync(goModData []byte, currentVersion string) (bool, error) { + gomodversion, err := GetWailsVersionFromModFile(goModData) + if err != nil { + return false, err + } + if gomodversion == nil { + return false, fmt.Errorf("Unable to find Wails in go.mod") + } + + result, err := semver.NewVersion(currentVersion) + if err != nil || result == nil { + return false, fmt.Errorf("Unable to parse Wails version: %s", currentVersion) + } + + return !gomodversion.Equal(result), nil +} + +func UpdateGoModVersion(goModText []byte, currentVersion string) ([]byte, error) { + file, err := modfile.Parse("", goModText, nil) + if err != nil { + return nil, err + } + + err = file.AddRequire("github.com/wailsapp/wails/v2", currentVersion) + if err != nil { + return nil, err + } + + // Replace + if len(file.Replace) == 0 { + return file.Format() + } + + for _, req := range file.Replace { + if req.Syntax == nil { + continue + } + tokenPosition := 0 + if !req.Syntax.InBlock { + tokenPosition = 1 + } + if req.Syntax.Token[tokenPosition] == "github.com/wailsapp/wails/v2" { + version := req.Syntax.Token[tokenPosition+1] + _, err := semver.NewVersion(version) + if err == nil { + req.Syntax.Token[tokenPosition+1] = currentVersion + } + } + } + + return file.Format() +} + +func SyncGoVersion(goModText []byte, goVersion string) ([]byte, bool, error) { + file, err := modfile.Parse("", goModText, nil) + if err != nil { + return nil, false, err + } + + modVersion, err := semver.NewVersion(file.Go.Version) + if err != nil { + return nil, false, fmt.Errorf("Unable to parse Go version from go mod file: %s", err) + } + + targetVersion, err := semver.NewVersion(goVersion) + if err != nil { + return nil, false, fmt.Errorf("Unable to parse Go version: %s", targetVersion) + } + + if !targetVersion.GreaterThan(modVersion) { + return goModText, false, nil + } + + file.Go.Version = goVersion + file.Go.Syntax.Token[1] = goVersion + goModText, err = file.Format() + if err != nil { + return nil, false, err + } + + return goModText, true, nil +} diff --git a/v2/internal/gomod/gomod_data_unix.go b/v2/internal/gomod/gomod_data_unix.go new file mode 100644 index 000000000..c6004f486 --- /dev/null +++ b/v2/internal/gomod/gomod_data_unix.go @@ -0,0 +1,139 @@ +//go:build darwin || linux + +package gomod + +const basic string = `module changeme + +go 1.17 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.7 + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +` + +const basicUpdated string = `module changeme + +go 1.17 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.20 + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +` + +const multilineRequire = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +` + +const multilineReplace = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +` + +const multilineReplaceNoVersion = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace github.com/wailsapp/wails/v2 => /home/lea/wails/v2 +` + +const multilineReplaceNoVersionBlock = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace ( + github.com/wailsapp/wails/v2 => /home/lea/wails/v2 +) +` + +const multilineReplaceBlock = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +) +` + +const multilineRequireUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 +` + +const multilineReplaceUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace github.com/wailsapp/wails/v2 v2.0.0-beta.20 => /home/lea/wails/v2 +` + +const multilineReplaceNoVersionUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace github.com/wailsapp/wails/v2 => /home/lea/wails/v2 +` + +const multilineReplaceNoVersionBlockUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace ( + github.com/wailsapp/wails/v2 => /home/lea/wails/v2 +) +` + +const multilineReplaceBlockUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 => /home/lea/wails/v2 +) +` diff --git a/v2/internal/gomod/gomod_data_windows.go b/v2/internal/gomod/gomod_data_windows.go new file mode 100644 index 000000000..691129c78 --- /dev/null +++ b/v2/internal/gomod/gomod_data_windows.go @@ -0,0 +1,135 @@ +//go:build windows + +package gomod + +const basic string = `module changeme + +go 1.17 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.7 + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` +const basicUpdated string = `module changeme + +go 1.17 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.20 + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` + +const multilineRequire = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` +const multilineReplace = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` + +const multilineReplaceNoVersion = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` + +const multilineReplaceNoVersionBlock = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace ( + github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +) +` + +const multilineReplaceBlock = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 +) + +replace ( + github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +) +` + +const multilineRequireUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` + +const multilineReplaceUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace github.com/wailsapp/wails/v2 v2.0.0-beta.20 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` +const multilineReplaceNoVersionUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +` +const multilineReplaceNoVersionBlockUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace ( + github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +) +` + +const multilineReplaceBlockUpdated = `module changeme + +go 1.17 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 +) + +replace ( + github.com/wailsapp/wails/v2 v2.0.0-beta.20 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2 +) +` diff --git a/v2/internal/gomod/gomod_test.go b/v2/internal/gomod/gomod_test.go new file mode 100644 index 000000000..eeafd0f9a --- /dev/null +++ b/v2/internal/gomod/gomod_test.go @@ -0,0 +1,139 @@ +package gomod + +import ( + "reflect" + "testing" + + "github.com/Masterminds/semver" + "github.com/matryer/is" +) + +func TestGetWailsVersion(t *testing.T) { + tests := []struct { + name string + goModText []byte + want *semver.Version + wantErr bool + }{ + {"basic", []byte(basic), semver.MustParse("v2.0.0-beta.7"), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetWailsVersionFromModFile(tt.goModText) + if (err != nil) != tt.wantErr { + t.Errorf("GetWailsVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetWailsVersion() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUpdateGoModVersion(t *testing.T) { + is2 := is.New(t) + + type args struct { + goModText []byte + currentVersion string + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + {"basic", args{[]byte(basic), "v2.0.0-beta.20"}, []byte(basicUpdated), false}, + {"basicmultiline", args{[]byte(multilineRequire), "v2.0.0-beta.20"}, []byte(multilineRequireUpdated), false}, + {"basicmultilinereplace", args{[]byte(multilineReplace), "v2.0.0-beta.20"}, []byte(multilineReplaceUpdated), false}, + {"basicmultilinereplaceblock", args{[]byte(multilineReplaceBlock), "v2.0.0-beta.20"}, []byte(multilineReplaceBlockUpdated), false}, + {"basicmultilinereplacenoversion", args{[]byte(multilineReplaceNoVersion), "v2.0.0-beta.20"}, []byte(multilineReplaceNoVersionUpdated), false}, + {"basicmultilinereplacenoversionblock", args{[]byte(multilineReplaceNoVersionBlock), "v2.0.0-beta.20"}, []byte(multilineReplaceNoVersionBlockUpdated), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := UpdateGoModVersion(tt.args.goModText, tt.args.currentVersion) + if (err != nil) != tt.wantErr { + t.Errorf("UpdateGoModVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + is2.Equal(string(got), string(tt.want)) + }) + } +} + +func TestGoModOutOfSync(t *testing.T) { + is2 := is.New(t) + + type args struct { + goModData []byte + currentVersion string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + {"basic", args{[]byte(basic), "v2.0.0-beta.20"}, true, false}, + {"basicmultiline", args{[]byte(multilineRequire), "v2.0.0-beta.20"}, true, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GoModOutOfSync(tt.args.goModData, tt.args.currentVersion) + if (err != nil) != tt.wantErr { + t.Errorf("GoModOutOfSync() error = %v, wantErr %v", err, tt.wantErr) + return + } + is2.Equal(got, tt.want) + }) + } +} + +const basicGo118 string = `module changeme + +go 1.18 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.7 +` + +const basicGo119 string = `module changeme + +go 1.19 + +require github.com/wailsapp/wails/v2 v2.0.0-beta.7 +` + +func TestUpdateGoModGoVersion(t *testing.T) { + is2 := is.New(t) + + type args struct { + goModText []byte + currentVersion string + } + tests := []struct { + name string + args args + want []byte + updated bool + }{ + {"basic1.18", args{[]byte(basicGo118), "1.18"}, []byte(basicGo118), false}, + {"basic1.19", args{[]byte(basicGo119), "1.17"}, []byte(basicGo119), false}, + {"basic1.19", args{[]byte(basicGo119), "1.18"}, []byte(basicGo119), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, updated, err := SyncGoVersion(tt.args.goModText, tt.args.currentVersion) + if err != nil { + t.Errorf("UpdateGoModVersion() error = %v", err) + return + } + if updated != tt.updated { + t.Errorf("UpdateGoModVersion() updated = %t, want = %t", updated, tt.updated) + return + } + is2.Equal(got, tt.want) + }) + } +} diff --git a/v2/internal/goversion/build_constraint.go b/v2/internal/goversion/build_constraint.go new file mode 100644 index 000000000..5e1b9fcf5 --- /dev/null +++ b/v2/internal/goversion/build_constraint.go @@ -0,0 +1,10 @@ +//go:build !go1.18 +// +build !go1.18 + +package goversion + +const MinGoVersionRequired = "You need Go " + MinRequirement + " or newer to compile this program" + +func init() { + MinGoVersionRequired +} diff --git a/v2/internal/goversion/min.go b/v2/internal/goversion/min.go new file mode 100644 index 000000000..8c057b3c2 --- /dev/null +++ b/v2/internal/goversion/min.go @@ -0,0 +1,3 @@ +package goversion + +const MinRequirement string = "1.20" diff --git a/v2/internal/logger/custom_logger.go b/v2/internal/logger/custom_logger.go new file mode 100644 index 000000000..51e07c0fc --- /dev/null +++ b/v2/internal/logger/custom_logger.go @@ -0,0 +1,95 @@ +package logger + +import ( + "fmt" +) + +// CustomLogger defines what a user can do with a logger +type CustomLogger interface { + // Writeln writes directly to the output with no log level plus line ending + Writeln(message string) + + // Write writes directly to the output with no log level + Write(message string) + + // Trace level logging. Works like Sprintf. + Trace(format string, args ...interface{}) + + // Debug level logging. Works like Sprintf. + Debug(format string, args ...interface{}) + + // Info level logging. Works like Sprintf. + Info(format string, args ...interface{}) + + // Warning level logging. Works like Sprintf. + Warning(format string, args ...interface{}) + + // Error level logging. Works like Sprintf. + Error(format string, args ...interface{}) + + // Fatal level logging. Works like Sprintf. + Fatal(format string, args ...interface{}) +} + +// customLogger is a utlility to log messages to a number of destinations +type customLogger struct { + logger *Logger + name string +} + +// New creates a new customLogger. You may pass in a number of `io.Writer`s that +// are the targets for the logs +func newcustomLogger(logger *Logger, name string) *customLogger { + result := &customLogger{ + name: name, + logger: logger, + } + return result +} + +// Writeln writes directly to the output with no log level +// Appends a carriage return to the message +func (l *customLogger) Writeln(message string) { + l.logger.Writeln(message) +} + +// Write writes directly to the output with no log level +func (l *customLogger) Write(message string) { + l.logger.Write(message) +} + +// Trace level logging. Works like Sprintf. +func (l *customLogger) Trace(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Trace(format, args...) +} + +// Debug level logging. Works like Sprintf. +func (l *customLogger) Debug(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Debug(format, args...) +} + +// Info level logging. Works like Sprintf. +func (l *customLogger) Info(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Info(format, args...) +} + +// Warning level logging. Works like Sprintf. +func (l *customLogger) Warning(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Warning(format, args...) +} + +// Error level logging. Works like Sprintf. +func (l *customLogger) Error(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Error(format, args...) +} + +// Fatal level logging. Works like Sprintf. +func (l *customLogger) Fatal(format string, args ...interface{}) { + format = fmt.Sprintf("%s | %s", l.name, format) + l.logger.Fatal(format, args...) +} diff --git a/v2/internal/logger/default_logger.go b/v2/internal/logger/default_logger.go new file mode 100644 index 000000000..5c72ae209 --- /dev/null +++ b/v2/internal/logger/default_logger.go @@ -0,0 +1,107 @@ +package logger + +import ( + "fmt" + "os" + + "github.com/wailsapp/wails/v2/pkg/logger" +) + +// LogLevel is an alias for the public LogLevel +type LogLevel = logger.LogLevel + +// Logger is a utlility to log messages to a number of destinations +type Logger struct { + output logger.Logger + logLevel LogLevel + showLevelInLog bool +} + +// New creates a new Logger. You may pass in a number of `io.Writer`s that +// are the targets for the logs +func New(output logger.Logger) *Logger { + if output == nil { + output = logger.NewDefaultLogger() + } + result := &Logger{ + logLevel: logger.INFO, + showLevelInLog: true, + output: output, + } + + return result +} + +// CustomLogger creates a new custom logger that prints out a name/id +// before the messages +func (l *Logger) CustomLogger(name string) CustomLogger { + return newcustomLogger(l, name) +} + +// HideLogLevel removes the loglevel text from the start of each logged line +func (l *Logger) HideLogLevel() { + l.showLevelInLog = true +} + +// SetLogLevel sets the minimum level of logs that will be output +func (l *Logger) SetLogLevel(level LogLevel) { + l.logLevel = level +} + +// Writeln writes directly to the output with no log level +// Appends a carriage return to the message +func (l *Logger) Writeln(message string) { + l.output.Print(message) +} + +// Write writes directly to the output with no log level +func (l *Logger) Write(message string) { + l.output.Print(message) +} + +// Print writes directly to the output with no log level +// Appends a carriage return to the message +func (l *Logger) Print(message string) { + l.Write(message) +} + +// Trace level logging. Works like Sprintf. +func (l *Logger) Trace(format string, args ...interface{}) { + if l.logLevel <= logger.TRACE { + l.output.Trace(fmt.Sprintf(format, args...)) + } +} + +// Debug level logging. Works like Sprintf. +func (l *Logger) Debug(format string, args ...interface{}) { + if l.logLevel <= logger.DEBUG { + l.output.Debug(fmt.Sprintf(format, args...)) + } +} + +// Info level logging. Works like Sprintf. +func (l *Logger) Info(format string, args ...interface{}) { + if l.logLevel <= logger.INFO { + l.output.Info(fmt.Sprintf(format, args...)) + } +} + +// Warning level logging. Works like Sprintf. +func (l *Logger) Warning(format string, args ...interface{}) { + if l.logLevel <= logger.WARNING { + l.output.Warning(fmt.Sprintf(format, args...)) + } +} + +// Error level logging. Works like Sprintf. +func (l *Logger) Error(format string, args ...interface{}) { + if l.logLevel <= logger.ERROR { + l.output.Error(fmt.Sprintf(format, args...)) + } +} + +// Fatal level logging. Works like Sprintf. +func (l *Logger) Fatal(format string, args ...interface{}) { + l.output.Fatal(fmt.Sprintf(format, args...)) + os.Exit(1) +} diff --git a/v2/internal/menumanager/applicationmenu.go b/v2/internal/menumanager/applicationmenu.go new file mode 100644 index 000000000..4446a00cb --- /dev/null +++ b/v2/internal/menumanager/applicationmenu.go @@ -0,0 +1,49 @@ +package menumanager + +import "github.com/wailsapp/wails/v2/pkg/menu" + +func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error { + if applicationMenu == nil { + return nil + } + + m.applicationMenu = applicationMenu + + // Reset the menu map + m.applicationMenuItemMap = NewMenuItemMap() + + // Add the menu to the menu map + m.applicationMenuItemMap.AddMenu(applicationMenu) + + return m.processApplicationMenu() +} + +func (m *Manager) GetApplicationMenuJSON() string { + return m.applicationMenuJSON +} + +func (m *Manager) GetProcessedApplicationMenu() *WailsMenu { + return m.processedApplicationMenu +} + +// UpdateApplicationMenu reprocesses the application menu to pick up structure +// changes etc +// Returns the JSON representation of the updated menu +func (m *Manager) UpdateApplicationMenu() (string, error) { + m.applicationMenuItemMap = NewMenuItemMap() + m.applicationMenuItemMap.AddMenu(m.applicationMenu) + err := m.processApplicationMenu() + return m.applicationMenuJSON, err +} + +func (m *Manager) processApplicationMenu() error { + // Process the menu + m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu) + m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap) + applicationMenuJSON, err := m.processedApplicationMenu.AsJSON() + if err != nil { + return err + } + m.applicationMenuJSON = applicationMenuJSON + return nil +} diff --git a/v2/internal/menumanager/contextmenu.go b/v2/internal/menumanager/contextmenu.go new file mode 100644 index 000000000..f05bcdc49 --- /dev/null +++ b/v2/internal/menumanager/contextmenu.go @@ -0,0 +1,59 @@ +package menumanager + +import ( + "encoding/json" + "fmt" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +type ContextMenu struct { + ID string + ProcessedMenu *WailsMenu + menuItemMap *MenuItemMap + menu *menu.Menu +} + +func (t *ContextMenu) AsJSON() (string, error) { + data, err := json.Marshal(t) + if err != nil { + return "", err + } + return string(data), nil +} + +func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { + result := &ContextMenu{ + ID: contextMenu.ID, + menu: contextMenu.Menu, + menuItemMap: NewMenuItemMap(), + } + + result.menuItemMap.AddMenu(contextMenu.Menu) + result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) + + return result +} + +func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { + newContextMenu := NewContextMenu(contextMenu) + + // Save the references + m.contextMenus[contextMenu.ID] = newContextMenu + m.contextMenuPointers[contextMenu] = contextMenu.ID +} + +func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { + contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] + if !contextMenuKnown { + return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID) + } + + // Create the updated context menu + updatedContextMenu := NewContextMenu(contextMenu) + + // Save the reference + m.contextMenus[contextMenuID] = updatedContextMenu + + return updatedContextMenu.AsJSON() +} diff --git a/v2/internal/menumanager/menuitemmap.go b/v2/internal/menumanager/menuitemmap.go new file mode 100644 index 000000000..e4e291be6 --- /dev/null +++ b/v2/internal/menumanager/menuitemmap.go @@ -0,0 +1,76 @@ +package menumanager + +import ( + "fmt" + "strconv" + "sync" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// MenuItemMap holds a mapping between menuIDs and menu items +type MenuItemMap struct { + idToMenuItemMap map[string]*menu.MenuItem + menuItemToIDMap map[*menu.MenuItem]string + + // We use a simple counter to keep track of unique menu IDs + menuIDCounter int64 + menuIDCounterMutex sync.Mutex +} + +func NewMenuItemMap() *MenuItemMap { + result := &MenuItemMap{ + idToMenuItemMap: make(map[string]*menu.MenuItem), + menuItemToIDMap: make(map[*menu.MenuItem]string), + } + + return result +} + +func (m *MenuItemMap) AddMenu(menu *menu.Menu) { + if menu == nil { + return + } + for _, item := range menu.Items { + m.processMenuItem(item) + } +} + +func (m *MenuItemMap) Dump() { + println("idToMenuItemMap:") + for key, value := range m.idToMenuItemMap { + fmt.Printf(" %s\t%p\n", key, value) + } + println("\nmenuItemToIDMap") + for key, value := range m.menuItemToIDMap { + fmt.Printf(" %p\t%s\n", key, value) + } +} + +// GenerateMenuID returns a unique string ID for a menu item +func (m *MenuItemMap) generateMenuID() string { + m.menuIDCounterMutex.Lock() + result := strconv.FormatInt(m.menuIDCounter, 10) + m.menuIDCounter++ + m.menuIDCounterMutex.Unlock() + return result +} + +func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) { + if item.SubMenu != nil { + for _, submenuitem := range item.SubMenu.Items { + m.processMenuItem(submenuitem) + } + } + + // Create a unique ID for this menu item + menuID := m.generateMenuID() + + // Store references + m.idToMenuItemMap[menuID] = item + m.menuItemToIDMap[item] = menuID +} + +func (m *MenuItemMap) getMenuItemByID(menuId string) *menu.MenuItem { + return m.idToMenuItemMap[menuId] +} diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go new file mode 100644 index 000000000..0c6be0df2 --- /dev/null +++ b/v2/internal/menumanager/menumanager.go @@ -0,0 +1,115 @@ +package menumanager + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +type Manager struct { + // The application menu. + applicationMenu *menu.Menu + applicationMenuJSON string + processedApplicationMenu *WailsMenu + + // Our application menu mappings + applicationMenuItemMap *MenuItemMap + + // Context menus + contextMenus map[string]*ContextMenu + contextMenuPointers map[*menu.ContextMenu]string + + // Tray menu stores + trayMenus map[string]*TrayMenu + trayMenuPointers map[*menu.TrayMenu]string + + // Radio groups + radioGroups map[*menu.MenuItem][]*menu.MenuItem +} + +func NewManager() *Manager { + return &Manager{ + applicationMenuItemMap: NewMenuItemMap(), + contextMenus: make(map[string]*ContextMenu), + contextMenuPointers: make(map[*menu.ContextMenu]string), + trayMenus: make(map[string]*TrayMenu), + trayMenuPointers: make(map[*menu.TrayMenu]string), + radioGroups: make(map[*menu.MenuItem][]*menu.MenuItem), + } +} + +func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.MenuItem { + return menuMap.idToMenuItemMap[menuId] +} + +func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error { + var menuItemMap *MenuItemMap + + switch menuType { + case "ApplicationMenu": + menuItemMap = m.applicationMenuItemMap + case "ContextMenu": + contextMenu := m.contextMenus[parentID] + if contextMenu == nil { + return fmt.Errorf("unknown context menu: %s", parentID) + } + menuItemMap = contextMenu.menuItemMap + case "TrayMenu": + trayMenu := m.trayMenus[parentID] + if trayMenu == nil { + return fmt.Errorf("unknown tray menu: %s", parentID) + } + menuItemMap = trayMenu.menuItemMap + default: + return fmt.Errorf("unknown menutype: %s", menuType) + } + + // Get the menu item + menuItem := menuItemMap.getMenuItemByID(menuID) + if menuItem == nil { + return fmt.Errorf("Cannot process menuid %s - unknown", menuID) + } + + // Is the menu item a checkbox? + if menuItem.Type == menu.CheckboxType { + // Toggle state + menuItem.Checked = !menuItem.Checked + } + + if menuItem.Type == menu.RadioType { + println("Toggle radio") + // Get my radio group + for _, radioMenuItem := range m.radioGroups[menuItem] { + radioMenuItem.Checked = (radioMenuItem == menuItem) + } + } + + if menuItem.Click == nil { + // No callback + return fmt.Errorf("No callback for menu '%s'", menuItem.Label) + } + + // Create new Callback struct + callbackData := &menu.CallbackData{ + MenuItem: menuItem, + // ContextData: data, + } + + // Call back! + go menuItem.Click(callbackData) + + return nil +} + +func (m *Manager) processRadioGroups(processedMenu *WailsMenu, itemMap *MenuItemMap) { + for _, group := range processedMenu.RadioGroups { + radioGroupMenuItems := []*menu.MenuItem{} + for _, member := range group.Members { + item := m.getMenuItemByID(itemMap, member) + radioGroupMenuItems = append(radioGroupMenuItems, item) + } + for _, radioGroupMenuItem := range radioGroupMenuItems { + m.radioGroups[radioGroupMenuItem] = radioGroupMenuItems + } + } +} diff --git a/v2/internal/menumanager/processedMenu.go b/v2/internal/menumanager/processedMenu.go new file mode 100644 index 000000000..c87646ccb --- /dev/null +++ b/v2/internal/menumanager/processedMenu.go @@ -0,0 +1,185 @@ +package menumanager + +import ( + "encoding/json" + + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/menu/keys" +) + +type ProcessedMenuItem struct { + ID string + // Label is what appears as the menu text + Label string `json:",omitempty"` + // Role is a predefined menu type + // Role menu.Role `json:",omitempty"` + // Accelerator holds a representation of a key binding + Accelerator *keys.Accelerator `json:",omitempty"` + // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu + Type menu.Type + // Disabled makes the item unselectable + Disabled bool `json:",omitempty"` + // Hidden ensures that the item is not shown in the menu + Hidden bool `json:",omitempty"` + // Checked indicates if the item is selected (used by Checkbox and Radio types only) + Checked bool `json:",omitempty"` + // SubMenu contains a list of menu items that will be shown as a submenu + // SubMenu []*MenuItem `json:"SubMenu,omitempty"` + SubMenu *ProcessedMenu `json:",omitempty"` + /* + // Colour + RGBA string `json:",omitempty"` + + // Font + FontSize int `json:",omitempty"` + FontName string `json:",omitempty"` + + // Image - base64 image data + Image string `json:",omitempty"` + MacTemplateImage bool `json:", omitempty"` + MacAlternate bool `json:", omitempty"` + + // Tooltip + Tooltip string `json:",omitempty"` + + // Styled label + StyledLabel []*ansi.StyledText `json:",omitempty"` + */ +} + +func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem { + ID := menuItemMap.menuItemToIDMap[menuItem] + + // Parse ANSI text + //var styledLabel []*ansi.StyledText + //tempLabel := menuItem.Label + //if strings.Contains(tempLabel, "\033[") { + // parsedLabel, err := ansi.Parse(menuItem.Label) + // if err == nil { + // styledLabel = parsedLabel + // } + //} + + result := &ProcessedMenuItem{ + ID: ID, + Label: menuItem.Label, + // Role: menuItem.Role, + Accelerator: menuItem.Accelerator, + Type: menuItem.Type, + Disabled: menuItem.Disabled, + Hidden: menuItem.Hidden, + Checked: menuItem.Checked, + SubMenu: nil, + // BackgroundColour: menuItem.BackgroundColour, + // FontSize: menuItem.FontSize, + // FontName: menuItem.FontName, + // Image: menuItem.Image, + // MacTemplateImage: menuItem.MacTemplateImage, + // MacAlternate: menuItem.MacAlternate, + // Tooltip: menuItem.Tooltip, + // StyledLabel: styledLabel, + } + + if menuItem.SubMenu != nil { + result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu) + } + + return result +} + +type ProcessedMenu struct { + Items []*ProcessedMenuItem +} + +func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu { + result := &ProcessedMenu{} + if menu != nil { + for _, item := range menu.Items { + processedMenuItem := NewProcessedMenuItem(menuItemMap, item) + result.Items = append(result.Items, processedMenuItem) + } + } + + return result +} + +// WailsMenu is the original menu with the addition +// of radio groups extracted from the menu data +type WailsMenu struct { + Menu *ProcessedMenu + RadioGroups []*RadioGroup + currentRadioGroup []string +} + +// RadioGroup holds all the members of the same radio group +type RadioGroup struct { + Members []string + Length int +} + +func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu { + result := &WailsMenu{} + + // Process the menus + result.Menu = NewProcessedMenu(menuItemMap, menu) + + // Process the radio groups + result.processRadioGroups() + + return result +} + +func (w *WailsMenu) AsJSON() (string, error) { + menuAsJSON, err := json.Marshal(w) + if err != nil { + return "", err + } + return string(menuAsJSON), nil +} + +func (w *WailsMenu) processRadioGroups() { + // Loop over top level menus + for _, item := range w.Menu.Items { + // Process MenuItem + w.processMenuItem(item) + } + + w.finaliseRadioGroup() +} + +func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) { + switch item.Type { + + // We need to recurse submenus + case menu.SubmenuType: + + // Finalise any current radio groups as they don't trickle down to submenus + w.finaliseRadioGroup() + + // Process each submenu item + for _, subitem := range item.SubMenu.Items { + w.processMenuItem(subitem) + } + case menu.RadioType: + // Add the item to the radio group + w.currentRadioGroup = append(w.currentRadioGroup, item.ID) + default: + w.finaliseRadioGroup() + } +} + +func (w *WailsMenu) finaliseRadioGroup() { + // If we were processing a radio group, fix up the references + if len(w.currentRadioGroup) > 0 { + + // Create new radiogroup + group := &RadioGroup{ + Members: w.currentRadioGroup, + Length: len(w.currentRadioGroup), + } + w.RadioGroups = append(w.RadioGroups, group) + + // Empty the radio group + w.currentRadioGroup = []string{} + } +} diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go new file mode 100644 index 000000000..5efc4a861 --- /dev/null +++ b/v2/internal/menumanager/traymenu.go @@ -0,0 +1,222 @@ +package menumanager + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/leaanthony/go-ansi-parser" + + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +var ( + trayMenuID int + trayMenuIDMutex sync.Mutex +) + +func generateTrayID() string { + var idStr string + trayMenuIDMutex.Lock() + idStr = strconv.Itoa(trayMenuID) + trayMenuID++ + trayMenuIDMutex.Unlock() + return idStr +} + +type TrayMenu struct { + ID string + Label string + FontSize int + FontName string + Disabled bool + Tooltip string `json:",omitempty"` + Image string + MacTemplateImage bool + RGBA string + menuItemMap *MenuItemMap + menu *menu.Menu + ProcessedMenu *WailsMenu + trayMenu *menu.TrayMenu + StyledLabel []*ansi.StyledText `json:",omitempty"` +} + +func (t *TrayMenu) AsJSON() (string, error) { + data, err := json.Marshal(t) + if err != nil { + return "", err + } + return string(data), nil +} + +func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { + // Parse ANSI text + var styledLabel []*ansi.StyledText + tempLabel := trayMenu.Label + if strings.Contains(tempLabel, "\033[") { + parsedLabel, err := ansi.Parse(tempLabel) + if err == nil { + styledLabel = parsedLabel + } + } + + result := &TrayMenu{ + Label: trayMenu.Label, + FontName: trayMenu.FontName, + FontSize: trayMenu.FontSize, + Disabled: trayMenu.Disabled, + Tooltip: trayMenu.Tooltip, + Image: trayMenu.Image, + MacTemplateImage: trayMenu.MacTemplateImage, + menu: trayMenu.Menu, + RGBA: trayMenu.RGBA, + menuItemMap: NewMenuItemMap(), + trayMenu: trayMenu, + StyledLabel: styledLabel, + } + + result.menuItemMap.AddMenu(trayMenu.Menu) + result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) + + return result +} + +func (m *Manager) OnTrayMenuOpen(id string) { + trayMenu, ok := m.trayMenus[id] + if !ok { + return + } + if trayMenu.trayMenu.OnOpen == nil { + return + } + go trayMenu.trayMenu.OnOpen() +} + +func (m *Manager) OnTrayMenuClose(id string) { + trayMenu, ok := m.trayMenus[id] + if !ok { + return + } + if trayMenu.trayMenu.OnClose == nil { + return + } + go trayMenu.trayMenu.OnClose() +} + +func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { + newTrayMenu := NewTrayMenu(trayMenu) + + // Hook up a new ID + trayID := generateTrayID() + newTrayMenu.ID = trayID + + // Save the references + m.trayMenus[trayID] = newTrayMenu + m.trayMenuPointers[trayMenu] = trayID + + return newTrayMenu.AsJSON() +} + +func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { + trayID, exists := m.trayMenuPointers[trayMenu] + if !exists { + return "", fmt.Errorf("Unable to find menu ID for tray menu!") + } + return trayID, nil +} + +// SetTrayMenu updates or creates a menu +func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { + trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] + if !trayMenuKnown { + return m.AddTrayMenu(trayMenu) + } + + // Create the updated tray menu + updatedTrayMenu := NewTrayMenu(trayMenu) + updatedTrayMenu.ID = trayID + + // Save the reference + m.trayMenus[trayID] = updatedTrayMenu + + return updatedTrayMenu.AsJSON() +} + +func (m *Manager) GetTrayMenus() ([]string, error) { + result := []string{} + for _, trayMenu := range m.trayMenus { + JSON, err := trayMenu.AsJSON() + if err != nil { + return nil, err + } + result = append(result, JSON) + } + + return result, nil +} + +func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { + trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] + if !trayMenuKnown { + return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) + } + + type LabelUpdate struct { + ID string + Label string `json:",omitempty"` + FontName string `json:",omitempty"` + FontSize int + RGBA string `json:",omitempty"` + Disabled bool + Tooltip string `json:",omitempty"` + Image string `json:",omitempty"` + MacTemplateImage bool + StyledLabel []*ansi.StyledText `json:",omitempty"` + } + + // Parse ANSI text + var styledLabel []*ansi.StyledText + tempLabel := trayMenu.Label + if strings.Contains(tempLabel, "\033[") { + parsedLabel, err := ansi.Parse(tempLabel) + if err == nil { + styledLabel = parsedLabel + } + } + + update := &LabelUpdate{ + ID: trayID, + Label: trayMenu.Label, + FontName: trayMenu.FontName, + FontSize: trayMenu.FontSize, + Disabled: trayMenu.Disabled, + Tooltip: trayMenu.Tooltip, + Image: trayMenu.Image, + MacTemplateImage: trayMenu.MacTemplateImage, + RGBA: trayMenu.RGBA, + StyledLabel: styledLabel, + } + + data, err := json.Marshal(update) + if err != nil { + return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") + } + + return string(data), nil +} + +func (m *Manager) GetContextMenus() ([]string, error) { + result := []string{} + for _, contextMenu := range m.contextMenus { + JSON, err := contextMenu.AsJSON() + if err != nil { + return nil, err + } + result = append(result, JSON) + } + + return result, nil +} diff --git a/v2/internal/platform/menu/manager.go b/v2/internal/platform/menu/manager.go new file mode 100644 index 000000000..0ddbc9dde --- /dev/null +++ b/v2/internal/platform/menu/manager.go @@ -0,0 +1,147 @@ +//go:build windows + +package menu + +import ( + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// MenuManager manages the menus for the application +var MenuManager = NewManager() + +type radioGroup []*menu.MenuItem + +// Click updates the radio group state based on the item clicked +func (g *radioGroup) Click(item *menu.MenuItem) { + for _, radioGroupItem := range *g { + if radioGroupItem != item { + radioGroupItem.Checked = false + } + } +} + +type processedMenu struct { + + // the menu we processed + menu *menu.Menu + + // updateMenuItemCallback is called when the menu item needs to be updated in the UI + updateMenuItemCallback func(*menu.MenuItem) + + // items is a map of all menu items in this menu + items map[*menu.MenuItem]struct{} + + // radioGroups tracks which radiogroup a menu item belongs to + radioGroups map[*menu.MenuItem][]*radioGroup +} + +func newProcessedMenu(topLevelMenu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) *processedMenu { + result := &processedMenu{ + updateMenuItemCallback: updateMenuItemCallback, + menu: topLevelMenu, + items: make(map[*menu.MenuItem]struct{}), + radioGroups: make(map[*menu.MenuItem][]*radioGroup), + } + result.process(topLevelMenu.Items) + return result +} + +func (p *processedMenu) process(items []*menu.MenuItem) { + var currentRadioGroup radioGroup + for index, item := range items { + // Save the reference to the top level menu for this item + p.items[item] = struct{}{} + + // If this is a radio item, add it to the radio group + if item.Type == menu.RadioType { + currentRadioGroup = append(currentRadioGroup, item) + } + + // If this is not a radio item, or we are processing the last item in the menu, + // then we need to add the current radio group to the map if it has items + if item.Type != menu.RadioType || index == len(items)-1 { + if len(currentRadioGroup) > 0 { + p.addRadioGroup(currentRadioGroup) + currentRadioGroup = nil + } + } + + // Process the submenu + if item.SubMenu != nil { + p.process(item.SubMenu.Items) + } + } +} + +func (p *processedMenu) processClick(item *menu.MenuItem) { + // If this item is not in our menu, then we can't process it + if _, ok := p.items[item]; !ok { + return + } + + // If this is a radio item, then we need to update the radio group + if item.Type == menu.RadioType { + // Get the radio groups for this item + radioGroups := p.radioGroups[item] + // Iterate each radio group this item belongs to and set the checked state + // of all items apart from the one that was clicked to false + for _, thisRadioGroup := range radioGroups { + thisRadioGroup.Click(item) + for _, thisRadioGroupItem := range *thisRadioGroup { + p.updateMenuItemCallback(thisRadioGroupItem) + } + } + } + + if item.Type == menu.CheckboxType { + p.updateMenuItemCallback(item) + } + +} + +func (p *processedMenu) addRadioGroup(r radioGroup) { + for _, item := range r { + p.radioGroups[item] = append(p.radioGroups[item], &r) + } +} + +type Manager struct { + menus map[*menu.Menu]*processedMenu +} + +func NewManager() *Manager { + return &Manager{ + menus: make(map[*menu.Menu]*processedMenu), + } +} + +func (m *Manager) AddMenu(menu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) { + m.menus[menu] = newProcessedMenu(menu, updateMenuItemCallback) +} + +func (m *Manager) ProcessClick(item *menu.MenuItem) { + + // if menuitem is a checkbox, then we need to toggle the state + if item.Type == menu.CheckboxType { + item.Checked = !item.Checked + } + + // Set the radio item to checked + if item.Type == menu.RadioType { + item.Checked = true + } + + for _, thisMenu := range m.menus { + thisMenu.processClick(item) + } + + if item.Click != nil { + item.Click(&menu.CallbackData{ + MenuItem: item, + }) + } +} + +func (m *Manager) RemoveMenu(data *menu.Menu) { + delete(m.menus, data) +} diff --git a/v2/internal/platform/menu/manager_test.go b/v2/internal/platform/menu/manager_test.go new file mode 100644 index 000000000..9e014b3ee --- /dev/null +++ b/v2/internal/platform/menu/manager_test.go @@ -0,0 +1,297 @@ +//go:build windows + +package menu_test + +import ( + "github.com/stretchr/testify/require" + platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu" + "github.com/wailsapp/wails/v2/pkg/menu" + "testing" +) + +func TestManager_ProcessClick_Checkbox(t *testing.T) { + + checkbox := menu.Label("Checkbox").SetChecked(false) + menu1 := &menu.Menu{ + Items: []*menu.MenuItem{ + checkbox, + }, + } + menu2 := &menu.Menu{ + Items: []*menu.MenuItem{ + checkbox, + }, + } + menuWithNoCheckbox := &menu.Menu{ + Items: []*menu.MenuItem{ + menu.Label("No Checkbox"), + }, + } + clicked := false + + tests := []struct { + name string + inputs []*menu.Menu + startState bool + expectedState bool + expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem + click func(*menu.CallbackData) + }{ + { + name: "should callback menu checkbox state when clicked (false -> true)", + inputs: []*menu.Menu{menu1}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + startState: false, + expectedState: true, + }, + { + name: "should callback multiple menus when checkbox state when clicked (false -> true)", + inputs: []*menu.Menu{menu1, menu2}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + }, + { + name: "should callback only for the menus that the checkbox is in (false -> true)", + inputs: []*menu.Menu{menu1, menuWithNoCheckbox}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + }, + { + name: "should callback menu checkbox state when clicked (true->false)", + inputs: []*menu.Menu{menu1}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + startState: true, + expectedState: false, + }, + { + name: "should callback multiple menus when checkbox state when clicked (true->false)", + inputs: []*menu.Menu{menu1, menu2}, + startState: true, + expectedState: false, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + }, + { + name: "should callback only for the menus that the checkbox is in (true->false)", + inputs: []*menu.Menu{menu1, menuWithNoCheckbox}, + startState: true, + expectedState: false, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + }, + }, + { + name: "should callback no menus if checkbox not in them", + inputs: []*menu.Menu{menuWithNoCheckbox}, + startState: false, + expectedState: false, + expectedMenuUpdates: nil, + }, + { + name: "should call Click on the checkbox", + inputs: []*menu.Menu{menu1, menu2}, + startState: false, + expectedState: true, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + menu1: {checkbox}, + menu2: {checkbox}, + }, + click: func(data *menu.CallbackData) { + clicked = true + }, + }, + } + for _, tt := range tests { + + menusUpdated := map[*menu.Menu][]*menu.MenuItem{} + clicked = false + + var checkMenuItemStateInMenu func(menu *menu.Menu) + + checkMenuItemStateInMenu = func(menu *menu.Menu) { + for _, item := range menusUpdated[menu] { + if item == checkbox { + require.Equal(t, tt.expectedState, item.Checked) + } + if item.SubMenu != nil { + checkMenuItemStateInMenu(item.SubMenu) + } + } + } + + t.Run(tt.name, func(t *testing.T) { + m := platformMenu.NewManager() + checkbox.SetChecked(tt.startState) + checkbox.Click = tt.click + for _, thisMenu := range tt.inputs { + thisMenu := thisMenu + m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) { + menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem) + }) + } + m.ProcessClick(checkbox) + + // Check the item has the correct state in all the menus + for thisMenu := range menusUpdated { + require.EqualValues(t, tt.expectedMenuUpdates[thisMenu], menusUpdated[thisMenu]) + } + + if tt.click != nil { + require.Equal(t, true, clicked) + } + }) + } +} + +func TestManager_ProcessClick_RadioGroups(t *testing.T) { + + radio1 := menu.Radio("Radio1", false, nil, nil) + radio2 := menu.Radio("Radio2", false, nil, nil) + radio3 := menu.Radio("Radio3", false, nil, nil) + radio4 := menu.Radio("Radio4", false, nil, nil) + radio5 := menu.Radio("Radio5", false, nil, nil) + radio6 := menu.Radio("Radio6", false, nil, nil) + + radioGroupOne := &menu.Menu{ + Items: []*menu.MenuItem{ + radio1, + radio2, + radio3, + }, + } + + radioGroupTwo := &menu.Menu{ + Items: []*menu.MenuItem{ + radio4, + radio5, + radio6, + }, + } + + radioGroupThree := &menu.Menu{ + Items: []*menu.MenuItem{ + radio1, + radio2, + radio3, + }, + } + + clicked := false + + tests := []struct { + name string + inputs []*menu.Menu + startState map[*menu.MenuItem]bool + selected *menu.MenuItem + expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem + click func(*menu.CallbackData) + expectedState map[*menu.MenuItem]bool + }{ + { + name: "should only set the clicked radio item", + inputs: []*menu.Menu{radioGroupOne}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + }, + }, + { + name: "should not affect other radio groups or menus", + inputs: []*menu.Menu{radioGroupOne, radioGroupTwo}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + radio4: true, + radio5: false, + radio6: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + radio4: true, + radio5: false, + radio6: false, + }, + }, + { + name: "menus with the same radio group should be updated", + inputs: []*menu.Menu{radioGroupOne, radioGroupThree}, + expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{ + radioGroupOne: {radio1, radio2, radio3}, + radioGroupThree: {radio1, radio2, radio3}, + }, + startState: map[*menu.MenuItem]bool{ + radio1: true, + radio2: false, + radio3: false, + }, + selected: radio2, + expectedState: map[*menu.MenuItem]bool{ + radio1: false, + radio2: true, + radio3: false, + }, + }, + } + for _, tt := range tests { + + menusUpdated := map[*menu.Menu][]*menu.MenuItem{} + clicked = false + + t.Run(tt.name, func(t *testing.T) { + m := platformMenu.NewManager() + + for item, value := range tt.startState { + item.SetChecked(value) + } + + tt.selected.Click = tt.click + for _, thisMenu := range tt.inputs { + thisMenu := thisMenu + m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) { + menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem) + }) + } + m.ProcessClick(tt.selected) + require.Equal(t, tt.expectedMenuUpdates, menusUpdated) + + // Check the items have the correct state in all the menus + for item, expectedValue := range tt.expectedState { + require.Equal(t, expectedValue, item.Checked) + } + + if tt.click != nil { + require.Equal(t, true, clicked) + } + }) + } +} diff --git a/v2/internal/platform/menu/windows.go b/v2/internal/platform/menu/windows.go new file mode 100644 index 000000000..68ebbcb49 --- /dev/null +++ b/v2/internal/platform/menu/windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package menu + +import "github.com/wailsapp/wails/v2/internal/platform/win32" + +type Menu struct { + menu win32.HMENU +} diff --git a/v2/internal/platform/win32/consts.go b/v2/internal/platform/win32/consts.go new file mode 100644 index 000000000..43149b036 --- /dev/null +++ b/v2/internal/platform/win32/consts.go @@ -0,0 +1,859 @@ +//go:build windows + +package win32 + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "golang.org/x/sys/windows" +) + +var ( + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetModuleHandle = modKernel32.NewProc("GetModuleHandleW") + + moduser32 = syscall.NewLazyDLL("user32.dll") + procRegisterClassEx = moduser32.NewProc("RegisterClassExW") + procLoadIcon = moduser32.NewProc("LoadIconW") + procLoadCursor = moduser32.NewProc("LoadCursorW") + procCreateWindowEx = moduser32.NewProc("CreateWindowExW") + procPostMessage = moduser32.NewProc("PostMessageW") + procGetCursorPos = moduser32.NewProc("GetCursorPos") + procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow") + procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") + procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu") + procDestroyMenu = moduser32.NewProc("DestroyMenu") + procAppendMenuW = moduser32.NewProc("AppendMenuW") + procCheckMenuItem = moduser32.NewProc("CheckMenuItem") + procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") + procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx") + procGetMessageW = moduser32.NewProc("GetMessageW") + procIsDialogMessage = moduser32.NewProc("IsDialogMessageW") + procTranslateMessage = moduser32.NewProc("TranslateMessage") + procDispatchMessage = moduser32.NewProc("DispatchMessageW") + procPostQuitMessage = moduser32.NewProc("PostQuitMessage") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procSetWindowCompositionAttribute = moduser32.NewProc("SetWindowCompositionAttribute") + procGetKeyState = moduser32.NewProc("GetKeyState") + procCreateAcceleratorTable = moduser32.NewProc("CreateAcceleratorTableW") + procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW") + + modshell32 = syscall.NewLazyDLL("shell32.dll") + procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") + + moddwmapi = syscall.NewLazyDLL("dwmapi.dll") + procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") + + moduxtheme = syscall.NewLazyDLL("uxtheme.dll") + procSetWindowTheme = moduxtheme.NewProc("SetWindowTheme") + + AllowDarkModeForWindow func(HWND, bool) uintptr + SetPreferredAppMode func(int32) uintptr +) + +type PreferredAppMode = int32 + +const ( + PreferredAppModeDefault PreferredAppMode = iota + PreferredAppModeAllowDark + PreferredAppModeForceDark + PreferredAppModeForceLight + PreferredAppModeMax +) + +/* +RtlGetNtVersionNumbers = void (LPDWORD major, LPDWORD minor, LPDWORD build) // 1809 17763 +ShouldAppsUseDarkMode = bool () // ordinal 132 +AllowDarkModeForWindow = bool (HWND hWnd, bool allow) // ordinal 133 +AllowDarkModeForApp = bool (bool allow) // ordinal 135, removed since 18334 +FlushMenuThemes = void () // ordinal 136 +RefreshImmersiveColorPolicyState = void () // ordinal 104 +IsDarkModeAllowedForWindow = bool (HWND hWnd) // ordinal 137 +GetIsImmersiveColorUsingHighContrast = bool (IMMERSIVE_HC_CACHE_MODE mode) // ordinal 106 +OpenNcThemeData = HTHEME (HWND hWnd, LPCWSTR pszClassList) // ordinal 49 +// Insider 18290 +ShouldSystemUseDarkMode = bool () // ordinal 138 +// Insider 18334 +SetPreferredAppMode = PreferredAppMode (PreferredAppMode appMode) // ordinal 135, since 18334 +IsDarkModeAllowedForApp = bool () // ordinal 139 +*/ +func Init() { + if IsWindowsVersionAtLeast(10, 0, 18334) { + + // AllowDarkModeForWindow is only available on Windows 10+ + uxtheme, err := windows.LoadLibrary("uxtheme.dll") + if err == nil { + procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(133)) + if err == nil { + AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr { + var allowInt int32 + if allow { + allowInt = 1 + } + ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(hwnd), uintptr(allowInt)) + return ret + } + } + } + + // SetPreferredAppMode is only available on Windows 10+ + procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(135)) + if err == nil { + SetPreferredAppMode = func(mode int32) uintptr { + ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode)) + return ret + } + SetPreferredAppMode(PreferredAppModeAllowDark) + } + } + +} + +type HANDLE uintptr +type HINSTANCE = HANDLE +type HICON = HANDLE +type HCURSOR = HANDLE +type HBRUSH = HANDLE +type HWND = HANDLE +type HMENU = HANDLE +type DWORD = uint32 +type ATOM uint16 +type MenuID uint16 + +const ( + WM_APP = 32768 + WM_ACTIVATE = 6 + WM_ACTIVATEAPP = 28 + WM_AFXFIRST = 864 + WM_AFXLAST = 895 + WM_ASKCBFORMATNAME = 780 + WM_CANCELJOURNAL = 75 + WM_CANCELMODE = 31 + WM_CAPTURECHANGED = 533 + WM_CHANGECBCHAIN = 781 + WM_CHAR = 258 + WM_CHARTOITEM = 47 + WM_CHILDACTIVATE = 34 + WM_CLEAR = 771 + WM_CLOSE = 16 + WM_COMMAND = 273 + WM_COMMNOTIFY = 68 /* OBSOLETE */ + WM_COMPACTING = 65 + WM_COMPAREITEM = 57 + WM_CONTEXTMENU = 123 + WM_COPY = 769 + WM_COPYDATA = 74 + WM_CREATE = 1 + WM_CTLCOLORBTN = 309 + WM_CTLCOLORDLG = 310 + WM_CTLCOLOREDIT = 307 + WM_CTLCOLORLISTBOX = 308 + WM_CTLCOLORMSGBOX = 306 + WM_CTLCOLORSCROLLBAR = 311 + WM_CTLCOLORSTATIC = 312 + WM_CUT = 768 + WM_DEADCHAR = 259 + WM_DELETEITEM = 45 + WM_DESTROY = 2 + WM_DESTROYCLIPBOARD = 775 + WM_DEVICECHANGE = 537 + WM_DEVMODECHANGE = 27 + WM_DISPLAYCHANGE = 126 + WM_DRAWCLIPBOARD = 776 + WM_DRAWITEM = 43 + WM_DROPFILES = 563 + WM_ENABLE = 10 + WM_ENDSESSION = 22 + WM_ENTERIDLE = 289 + WM_ENTERMENULOOP = 529 + WM_ENTERSIZEMOVE = 561 + WM_ERASEBKGND = 20 + WM_EXITMENULOOP = 530 + WM_EXITSIZEMOVE = 562 + WM_FONTCHANGE = 29 + WM_GETDLGCODE = 135 + WM_GETFONT = 49 + WM_GETHOTKEY = 51 + WM_GETICON = 127 + WM_GETMINMAXINFO = 36 + WM_GETTEXT = 13 + WM_GETTEXTLENGTH = 14 + WM_HANDHELDFIRST = 856 + WM_HANDHELDLAST = 863 + WM_HELP = 83 + WM_HOTKEY = 786 + WM_HSCROLL = 276 + WM_HSCROLLCLIPBOARD = 782 + WM_ICONERASEBKGND = 39 + WM_INITDIALOG = 272 + WM_INITMENU = 278 + WM_INITMENUPOPUP = 279 + WM_INPUT = 0x00FF + WM_INPUTLANGCHANGE = 81 + WM_INPUTLANGCHANGEREQUEST = 80 + WM_KEYDOWN = 256 + WM_KEYUP = 257 + WM_KILLFOCUS = 8 + WM_MDIACTIVATE = 546 + WM_MDICASCADE = 551 + WM_MDICREATE = 544 + WM_MDIDESTROY = 545 + WM_MDIGETACTIVE = 553 + WM_MDIICONARRANGE = 552 + WM_MDIMAXIMIZE = 549 + WM_MDINEXT = 548 + WM_MDIREFRESHMENU = 564 + WM_MDIRESTORE = 547 + WM_MDISETMENU = 560 + WM_MDITILE = 550 + WM_MEASUREITEM = 44 + WM_GETOBJECT = 0x003D + WM_CHANGEUISTATE = 0x0127 + WM_UPDATEUISTATE = 0x0128 + WM_QUERYUISTATE = 0x0129 + WM_UNINITMENUPOPUP = 0x0125 + WM_MENURBUTTONUP = 290 + WM_MENUCOMMAND = 0x0126 + WM_MENUGETOBJECT = 0x0124 + WM_MENUDRAG = 0x0123 + WM_APPCOMMAND = 0x0319 + WM_MENUCHAR = 288 + WM_MENUSELECT = 287 + WM_MOVE = 3 + WM_MOVING = 534 + WM_NCACTIVATE = 134 + WM_NCCALCSIZE = 131 + WM_NCCREATE = 129 + WM_NCDESTROY = 130 + WM_NCHITTEST = 132 + WM_NCLBUTTONDBLCLK = 163 + WM_NCLBUTTONDOWN = 161 + WM_NCLBUTTONUP = 162 + WM_NCMBUTTONDBLCLK = 169 + WM_NCMBUTTONDOWN = 167 + WM_NCMBUTTONUP = 168 + WM_NCXBUTTONDOWN = 171 + WM_NCXBUTTONUP = 172 + WM_NCXBUTTONDBLCLK = 173 + WM_NCMOUSEHOVER = 0x02A0 + WM_NCMOUSELEAVE = 0x02A2 + WM_NCMOUSEMOVE = 160 + WM_NCPAINT = 133 + WM_NCRBUTTONDBLCLK = 166 + WM_NCRBUTTONDOWN = 164 + WM_NCRBUTTONUP = 165 + WM_NEXTDLGCTL = 40 + WM_NEXTMENU = 531 + WM_NOTIFY = 78 + WM_NOTIFYFORMAT = 85 + WM_NULL = 0 + WM_PAINT = 15 + WM_PAINTCLIPBOARD = 777 + WM_PAINTICON = 38 + WM_PALETTECHANGED = 785 + WM_PALETTEISCHANGING = 784 + WM_PARENTNOTIFY = 528 + WM_PASTE = 770 + WM_PENWINFIRST = 896 + WM_PENWINLAST = 911 + WM_POWER = 72 + WM_PRINT = 791 + WM_PRINTCLIENT = 792 + WM_QUERYDRAGICON = 55 + WM_QUERYENDSESSION = 17 + WM_QUERYNEWPALETTE = 783 + WM_QUERYOPEN = 19 + WM_QUEUESYNC = 35 + WM_QUIT = 18 + WM_RENDERALLFORMATS = 774 + WM_RENDERFORMAT = 773 + WM_SETCURSOR = 32 + WM_SETFOCUS = 7 + WM_SETFONT = 48 + WM_SETHOTKEY = 50 + WM_SETICON = 128 + WM_SETREDRAW = 11 + WM_SETTEXT = 12 + WM_SETTINGCHANGE = 26 + WM_SHOWWINDOW = 24 + WM_SIZE = 5 + WM_SIZECLIPBOARD = 779 + WM_SIZING = 532 + WM_SPOOLERSTATUS = 42 + WM_STYLECHANGED = 125 + WM_STYLECHANGING = 124 + WM_SYSCHAR = 262 + WM_SYSCOLORCHANGE = 21 + WM_SYSCOMMAND = 274 + WM_SYSDEADCHAR = 263 + WM_SYSKEYDOWN = 260 + WM_SYSKEYUP = 261 + WM_TCARD = 82 + WM_THEMECHANGED = 794 + WM_TIMECHANGE = 30 + WM_TIMER = 275 + WM_UNDO = 772 + WM_USER = 1024 + WM_USERCHANGED = 84 + WM_VKEYTOITEM = 46 + WM_VSCROLL = 277 + WM_VSCROLLCLIPBOARD = 778 + WM_WINDOWPOSCHANGED = 71 + WM_WINDOWPOSCHANGING = 70 + WM_WININICHANGE = 26 + WM_KEYFIRST = 256 + WM_KEYLAST = 264 + WM_SYNCPAINT = 136 + WM_MOUSEACTIVATE = 33 + WM_MOUSEMOVE = 512 + WM_LBUTTONDOWN = 513 + WM_LBUTTONUP = 514 + WM_LBUTTONDBLCLK = 515 + WM_RBUTTONDOWN = 516 + WM_RBUTTONUP = 517 + WM_RBUTTONDBLCLK = 518 + WM_MBUTTONDOWN = 519 + WM_MBUTTONUP = 520 + WM_MBUTTONDBLCLK = 521 + WM_MOUSEWHEEL = 522 + WM_MOUSEFIRST = 512 + WM_XBUTTONDOWN = 523 + WM_XBUTTONUP = 524 + WM_XBUTTONDBLCLK = 525 + WM_MOUSELAST = 525 + WM_MOUSEHOVER = 0x2A1 + WM_MOUSELEAVE = 0x2A3 + WM_CLIPBOARDUPDATE = 0x031D + + WS_EX_APPWINDOW = 0x00040000 + WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000 + WS_EX_NOREDIRECTIONBITMAP = 0x00200000 + CW_USEDEFAULT = ^0x7fffffff + + NIM_ADD = 0x00000000 + NIM_MODIFY = 0x00000001 + NIM_DELETE = 0x00000002 + NIM_SETVERSION = 0x00000004 + + NIF_MESSAGE = 0x00000001 + NIF_ICON = 0x00000002 + NIF_TIP = 0x00000004 + NIF_STATE = 0x00000008 + NIF_INFO = 0x00000010 + + NIS_HIDDEN = 0x00000001 + + NIIF_NONE = 0x00000000 + NIIF_INFO = 0x00000001 + NIIF_WARNING = 0x00000002 + NIIF_ERROR = 0x00000003 + NIIF_USER = 0x00000004 + NIIF_NOSOUND = 0x00000010 + NIIF_LARGE_ICON = 0x00000020 + NIIF_RESPECT_QUIET_TIME = 0x00000080 + NIIF_ICON_MASK = 0x0000000F + + IMAGE_BITMAP = 0 + IMAGE_ICON = 1 + LR_LOADFROMFILE = 0x00000010 + LR_DEFAULTSIZE = 0x00000040 + + IDC_ARROW = 32512 + COLOR_WINDOW = 5 + COLOR_BTNFACE = 15 + + GWLP_USERDATA = -21 + WS_CLIPSIBLINGS = 0x04000000 + WS_EX_CONTROLPARENT = 0x00010000 + + HWND_MESSAGE = ^HWND(2) + NOTIFYICON_VERSION = 4 + + IDI_APPLICATION = 32512 + + MenuItemMsgID = WM_APP + 1024 + NotifyIconMessageId = WM_APP + iota + + MF_STRING = 0x00000000 + MF_ENABLED = 0x00000000 + MF_GRAYED = 0x00000001 + MF_DISABLED = 0x00000002 + MF_SEPARATOR = 0x00000800 + MF_UNCHECKED = 0x00000000 + MF_CHECKED = 0x00000008 + MF_POPUP = 0x00000010 + MF_MENUBARBREAK = 0x00000020 + MF_BYCOMMAND = 0x00000000 + + TPM_LEFTALIGN = 0x0000 + + CS_VREDRAW = 0x0001 + CS_HREDRAW = 0x0002 +) + +func WMMessageToString(msg uintptr) string { + // Convert windows message to string + switch msg { + case WM_APP: + return "WM_APP" + case WM_ACTIVATE: + return "WM_ACTIVATE" + case WM_ACTIVATEAPP: + return "WM_ACTIVATEAPP" + case WM_AFXFIRST: + return "WM_AFXFIRST" + case WM_AFXLAST: + return "WM_AFXLAST" + case WM_ASKCBFORMATNAME: + return "WM_ASKCBFORMATNAME" + case WM_CANCELJOURNAL: + return "WM_CANCELJOURNAL" + case WM_CANCELMODE: + return "WM_CANCELMODE" + case WM_CAPTURECHANGED: + return "WM_CAPTURECHANGED" + case WM_CHANGECBCHAIN: + return "WM_CHANGECBCHAIN" + case WM_CHAR: + return "WM_CHAR" + case WM_CHARTOITEM: + return "WM_CHARTOITEM" + case WM_CHILDACTIVATE: + return "WM_CHILDACTIVATE" + case WM_CLEAR: + return "WM_CLEAR" + case WM_CLOSE: + return "WM_CLOSE" + case WM_COMMAND: + return "WM_COMMAND" + case WM_COMMNOTIFY /* OBSOLETE */ : + return "WM_COMMNOTIFY" + case WM_COMPACTING: + return "WM_COMPACTING" + case WM_COMPAREITEM: + return "WM_COMPAREITEM" + case WM_CONTEXTMENU: + return "WM_CONTEXTMENU" + case WM_COPY: + return "WM_COPY" + case WM_COPYDATA: + return "WM_COPYDATA" + case WM_CREATE: + return "WM_CREATE" + case WM_CTLCOLORBTN: + return "WM_CTLCOLORBTN" + case WM_CTLCOLORDLG: + return "WM_CTLCOLORDLG" + case WM_CTLCOLOREDIT: + return "WM_CTLCOLOREDIT" + case WM_CTLCOLORLISTBOX: + return "WM_CTLCOLORLISTBOX" + case WM_CTLCOLORMSGBOX: + return "WM_CTLCOLORMSGBOX" + case WM_CTLCOLORSCROLLBAR: + return "WM_CTLCOLORSCROLLBAR" + case WM_CTLCOLORSTATIC: + return "WM_CTLCOLORSTATIC" + case WM_CUT: + return "WM_CUT" + case WM_DEADCHAR: + return "WM_DEADCHAR" + case WM_DELETEITEM: + return "WM_DELETEITEM" + case WM_DESTROY: + return "WM_DESTROY" + case WM_DESTROYCLIPBOARD: + return "WM_DESTROYCLIPBOARD" + case WM_DEVICECHANGE: + return "WM_DEVICECHANGE" + case WM_DEVMODECHANGE: + return "WM_DEVMODECHANGE" + case WM_DISPLAYCHANGE: + return "WM_DISPLAYCHANGE" + case WM_DRAWCLIPBOARD: + return "WM_DRAWCLIPBOARD" + case WM_DRAWITEM: + return "WM_DRAWITEM" + case WM_DROPFILES: + return "WM_DROPFILES" + case WM_ENABLE: + return "WM_ENABLE" + case WM_ENDSESSION: + return "WM_ENDSESSION" + case WM_ENTERIDLE: + return "WM_ENTERIDLE" + case WM_ENTERMENULOOP: + return "WM_ENTERMENULOOP" + case WM_ENTERSIZEMOVE: + return "WM_ENTERSIZEMOVE" + case WM_ERASEBKGND: + return "WM_ERASEBKGND" + case WM_EXITMENULOOP: + return "WM_EXITMENULOOP" + case WM_EXITSIZEMOVE: + return "WM_EXITSIZEMOVE" + case WM_FONTCHANGE: + return "WM_FONTCHANGE" + case WM_GETDLGCODE: + return "WM_GETDLGCODE" + case WM_GETFONT: + return "WM_GETFONT" + case WM_GETHOTKEY: + return "WM_GETHOTKEY" + case WM_GETICON: + return "WM_GETICON" + case WM_GETMINMAXINFO: + return "WM_GETMINMAXINFO" + case WM_GETTEXT: + return "WM_GETTEXT" + case WM_GETTEXTLENGTH: + return "WM_GETTEXTLENGTH" + case WM_HANDHELDFIRST: + return "WM_HANDHELDFIRST" + case WM_HANDHELDLAST: + return "WM_HANDHELDLAST" + case WM_HELP: + return "WM_HELP" + case WM_HOTKEY: + return "WM_HOTKEY" + case WM_HSCROLL: + return "WM_HSCROLL" + case WM_HSCROLLCLIPBOARD: + return "WM_HSCROLLCLIPBOARD" + case WM_ICONERASEBKGND: + return "WM_ICONERASEBKGND" + case WM_INITDIALOG: + return "WM_INITDIALOG" + case WM_INITMENU: + return "WM_INITMENU" + case WM_INITMENUPOPUP: + return "WM_INITMENUPOPUP" + case WM_INPUT: + return "WM_INPUT" + case WM_INPUTLANGCHANGE: + return "WM_INPUTLANGCHANGE" + case WM_INPUTLANGCHANGEREQUEST: + return "WM_INPUTLANGCHANGEREQUEST" + case WM_KEYDOWN: + return "WM_KEYDOWN" + case WM_KEYUP: + return "WM_KEYUP" + case WM_KILLFOCUS: + return "WM_KILLFOCUS" + case WM_MDIACTIVATE: + return "WM_MDIACTIVATE" + case WM_MDICASCADE: + return "WM_MDICASCADE" + case WM_MDICREATE: + return "WM_MDICREATE" + case WM_MDIDESTROY: + return "WM_MDIDESTROY" + case WM_MDIGETACTIVE: + return "WM_MDIGETACTIVE" + case WM_MDIICONARRANGE: + return "WM_MDIICONARRANGE" + case WM_MDIMAXIMIZE: + return "WM_MDIMAXIMIZE" + case WM_MDINEXT: + return "WM_MDINEXT" + case WM_MDIREFRESHMENU: + return "WM_MDIREFRESHMENU" + case WM_MDIRESTORE: + return "WM_MDIRESTORE" + case WM_MDISETMENU: + return "WM_MDISETMENU" + case WM_MDITILE: + return "WM_MDITILE" + case WM_MEASUREITEM: + return "WM_MEASUREITEM" + case WM_GETOBJECT: + return "WM_GETOBJECT" + case WM_CHANGEUISTATE: + return "WM_CHANGEUISTATE" + case WM_UPDATEUISTATE: + return "WM_UPDATEUISTATE" + case WM_QUERYUISTATE: + return "WM_QUERYUISTATE" + case WM_UNINITMENUPOPUP: + return "WM_UNINITMENUPOPUP" + case WM_MENURBUTTONUP: + return "WM_MENURBUTTONUP" + case WM_MENUCOMMAND: + return "WM_MENUCOMMAND" + case WM_MENUGETOBJECT: + return "WM_MENUGETOBJECT" + case WM_MENUDRAG: + return "WM_MENUDRAG" + case WM_APPCOMMAND: + return "WM_APPCOMMAND" + case WM_MENUCHAR: + return "WM_MENUCHAR" + case WM_MENUSELECT: + return "WM_MENUSELECT" + case WM_MOVE: + return "WM_MOVE" + case WM_MOVING: + return "WM_MOVING" + case WM_NCACTIVATE: + return "WM_NCACTIVATE" + case WM_NCCALCSIZE: + return "WM_NCCALCSIZE" + case WM_NCCREATE: + return "WM_NCCREATE" + case WM_NCDESTROY: + return "WM_NCDESTROY" + case WM_NCHITTEST: + return "WM_NCHITTEST" + case WM_NCLBUTTONDBLCLK: + return "WM_NCLBUTTONDBLCLK" + case WM_NCLBUTTONDOWN: + return "WM_NCLBUTTONDOWN" + case WM_NCLBUTTONUP: + return "WM_NCLBUTTONUP" + case WM_NCMBUTTONDBLCLK: + return "WM_NCMBUTTONDBLCLK" + case WM_NCMBUTTONDOWN: + return "WM_NCMBUTTONDOWN" + case WM_NCMBUTTONUP: + return "WM_NCMBUTTONUP" + case WM_NCXBUTTONDOWN: + return "WM_NCXBUTTONDOWN" + case WM_NCXBUTTONUP: + return "WM_NCXBUTTONUP" + case WM_NCXBUTTONDBLCLK: + return "WM_NCXBUTTONDBLCLK" + case WM_NCMOUSEHOVER: + return "WM_NCMOUSEHOVER" + case WM_NCMOUSELEAVE: + return "WM_NCMOUSELEAVE" + case WM_NCMOUSEMOVE: + return "WM_NCMOUSEMOVE" + case WM_NCPAINT: + return "WM_NCPAINT" + case WM_NCRBUTTONDBLCLK: + return "WM_NCRBUTTONDBLCLK" + case WM_NCRBUTTONDOWN: + return "WM_NCRBUTTONDOWN" + case WM_NCRBUTTONUP: + return "WM_NCRBUTTONUP" + case WM_NEXTDLGCTL: + return "WM_NEXTDLGCTL" + case WM_NEXTMENU: + return "WM_NEXTMENU" + case WM_NOTIFY: + return "WM_NOTIFY" + case WM_NOTIFYFORMAT: + return "WM_NOTIFYFORMAT" + case WM_NULL: + return "WM_NULL" + case WM_PAINT: + return "WM_PAINT" + case WM_PAINTCLIPBOARD: + return "WM_PAINTCLIPBOARD" + case WM_PAINTICON: + return "WM_PAINTICON" + case WM_PALETTECHANGED: + return "WM_PALETTECHANGED" + case WM_PALETTEISCHANGING: + return "WM_PALETTEISCHANGING" + case WM_PARENTNOTIFY: + return "WM_PARENTNOTIFY" + case WM_PASTE: + return "WM_PASTE" + case WM_PENWINFIRST: + return "WM_PENWINFIRST" + case WM_PENWINLAST: + return "WM_PENWINLAST" + case WM_POWER: + return "WM_POWER" + case WM_PRINT: + return "WM_PRINT" + case WM_PRINTCLIENT: + return "WM_PRINTCLIENT" + case WM_QUERYDRAGICON: + return "WM_QUERYDRAGICON" + case WM_QUERYENDSESSION: + return "WM_QUERYENDSESSION" + case WM_QUERYNEWPALETTE: + return "WM_QUERYNEWPALETTE" + case WM_QUERYOPEN: + return "WM_QUERYOPEN" + case WM_QUEUESYNC: + return "WM_QUEUESYNC" + case WM_QUIT: + return "WM_QUIT" + case WM_RENDERALLFORMATS: + return "WM_RENDERALLFORMATS" + case WM_RENDERFORMAT: + return "WM_RENDERFORMAT" + case WM_SETCURSOR: + return "WM_SETCURSOR" + case WM_SETFOCUS: + return "WM_SETFOCUS" + case WM_SETFONT: + return "WM_SETFONT" + case WM_SETHOTKEY: + return "WM_SETHOTKEY" + case WM_SETICON: + return "WM_SETICON" + case WM_SETREDRAW: + return "WM_SETREDRAW" + case WM_SETTEXT: + return "WM_SETTEXT" + case WM_SETTINGCHANGE: + return "WM_SETTINGCHANGE" + case WM_SHOWWINDOW: + return "WM_SHOWWINDOW" + case WM_SIZE: + return "WM_SIZE" + case WM_SIZECLIPBOARD: + return "WM_SIZECLIPBOARD" + case WM_SIZING: + return "WM_SIZING" + case WM_SPOOLERSTATUS: + return "WM_SPOOLERSTATUS" + case WM_STYLECHANGED: + return "WM_STYLECHANGED" + case WM_STYLECHANGING: + return "WM_STYLECHANGING" + case WM_SYSCHAR: + return "WM_SYSCHAR" + case WM_SYSCOLORCHANGE: + return "WM_SYSCOLORCHANGE" + case WM_SYSCOMMAND: + return "WM_SYSCOMMAND" + case WM_SYSDEADCHAR: + return "WM_SYSDEADCHAR" + case WM_SYSKEYDOWN: + return "WM_SYSKEYDOWN" + case WM_SYSKEYUP: + return "WM_SYSKEYUP" + case WM_TCARD: + return "WM_TCARD" + case WM_THEMECHANGED: + return "WM_THEMECHANGED" + case WM_TIMECHANGE: + return "WM_TIMECHANGE" + case WM_TIMER: + return "WM_TIMER" + case WM_UNDO: + return "WM_UNDO" + case WM_USER: + return "WM_USER" + case WM_USERCHANGED: + return "WM_USERCHANGED" + case WM_VKEYTOITEM: + return "WM_VKEYTOITEM" + case WM_VSCROLL: + return "WM_VSCROLL" + case WM_VSCROLLCLIPBOARD: + return "WM_VSCROLLCLIPBOARD" + case WM_WINDOWPOSCHANGED: + return "WM_WINDOWPOSCHANGED" + case WM_WINDOWPOSCHANGING: + return "WM_WINDOWPOSCHANGING" + case WM_KEYLAST: + return "WM_KEYLAST" + case WM_SYNCPAINT: + return "WM_SYNCPAINT" + case WM_MOUSEACTIVATE: + return "WM_MOUSEACTIVATE" + case WM_MOUSEMOVE: + return "WM_MOUSEMOVE" + case WM_LBUTTONDOWN: + return "WM_LBUTTONDOWN" + case WM_LBUTTONUP: + return "WM_LBUTTONUP" + case WM_LBUTTONDBLCLK: + return "WM_LBUTTONDBLCLK" + case WM_RBUTTONDOWN: + return "WM_RBUTTONDOWN" + case WM_RBUTTONUP: + return "WM_RBUTTONUP" + case WM_RBUTTONDBLCLK: + return "WM_RBUTTONDBLCLK" + case WM_MBUTTONDOWN: + return "WM_MBUTTONDOWN" + case WM_MBUTTONUP: + return "WM_MBUTTONUP" + case WM_MBUTTONDBLCLK: + return "WM_MBUTTONDBLCLK" + case WM_MOUSEWHEEL: + return "WM_MOUSEWHEEL" + case WM_XBUTTONDOWN: + return "WM_XBUTTONDOWN" + case WM_XBUTTONUP: + return "WM_XBUTTONUP" + case WM_MOUSELAST: + return "WM_MOUSELAST" + case WM_MOUSEHOVER: + return "WM_MOUSEHOVER" + case WM_MOUSELEAVE: + return "WM_MOUSELEAVE" + case WM_CLIPBOARDUPDATE: + return "WM_CLIPBOARDUPDATE" + default: + return fmt.Sprintf("0x%08x", msg) + } +} + +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return windowsVersion.Major >= major && + windowsVersion.Minor >= minor && + windowsVersion.Build >= buildNumber +} + +type WindowProc func(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr + +func GetModuleHandle(value uintptr) uintptr { + result, _, _ := procGetModuleHandle.Call(value) + return result +} + +func GetMessage(msg *MSG) uintptr { + rt, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0) + return rt +} + +func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { + ret, _, _ := procPostMessage.Call( + uintptr(hwnd), + uintptr(msg), + wParam, + lParam) + + return ret +} + +func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool { + ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid))) + return ret == 1 +} + +func IsDialogMessage(hwnd HWND, msg *MSG) uintptr { + ret, _, _ := procIsDialogMessage.Call(uintptr(hwnd), uintptr(unsafe.Pointer(msg))) + return ret +} + +func TranslateMessage(msg *MSG) uintptr { + ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg))) + return ret +} + +func DispatchMessage(msg *MSG) uintptr { + ret, _, _ := procDispatchMessage.Call(uintptr(unsafe.Pointer(msg))) + return ret +} + +func PostQuitMessage(exitCode int32) { + procPostQuitMessage.Call(uintptr(exitCode)) +} + +func LoHiWords(input uint32) (uint16, uint16) { + return uint16(input & 0xffff), uint16(input >> 16 & 0xffff) +} diff --git a/v2/internal/platform/win32/cursor.go b/v2/internal/platform/win32/cursor.go new file mode 100644 index 000000000..04449a91b --- /dev/null +++ b/v2/internal/platform/win32/cursor.go @@ -0,0 +1,11 @@ +//go:build windows + +package win32 + +import "unsafe" + +func GetCursorPos() (x, y int, ok bool) { + pt := POINT{} + ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) + return int(pt.X), int(pt.Y), ret != 0 +} diff --git a/v2/internal/platform/win32/icon.go b/v2/internal/platform/win32/icon.go new file mode 100644 index 000000000..916b92d44 --- /dev/null +++ b/v2/internal/platform/win32/icon.go @@ -0,0 +1,41 @@ +//go:build windows + +package win32 + +import ( + "unsafe" +) + +func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) { + icon := 0 + if isIcon { + icon = 1 + } + r, _, err := procCreateIconFromResourceEx.Call( + presbits, + uintptr(dwResSize), + uintptr(icon), + uintptr(version), + uintptr(cxDesired), + uintptr(cyDesired), + uintptr(flags), + ) + + if r == 0 { + return 0, err + } + return r, nil +} + +// CreateHIconFromPNG creates a HICON from a PNG file +func CreateHIconFromPNG(pngData []byte) (HICON, error) { + icon, err := CreateIconFromResourceEx( + uintptr(unsafe.Pointer(&pngData[0])), + uint32(len(pngData)), + true, + 0x00030000, + 0, + 0, + LR_DEFAULTSIZE) + return HICON(icon), err +} diff --git a/v2/internal/platform/win32/keyboard.go b/v2/internal/platform/win32/keyboard.go new file mode 100644 index 000000000..7a86d6643 --- /dev/null +++ b/v2/internal/platform/win32/keyboard.go @@ -0,0 +1,810 @@ +//go:build windows + +/* + * Copyright (C) 2019 The Winc Authors. All Rights Reserved. + * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved. + */ + +package win32 + +import ( + "bytes" + "github.com/wailsapp/wails/v2/pkg/menu/keys" + "strings" + "unsafe" +) + +type Key uint16 + +func (k Key) String() string { + return key2string[k] +} + +// Virtual key codes +const ( + VK_LBUTTON = 1 + VK_RBUTTON = 2 + VK_CANCEL = 3 + VK_MBUTTON = 4 + VK_XBUTTON1 = 5 + VK_XBUTTON2 = 6 + VK_BACK = 8 + VK_TAB = 9 + VK_CLEAR = 12 + VK_RETURN = 13 + VK_SHIFT = 16 + VK_CONTROL = 17 + VK_MENU = 18 + VK_PAUSE = 19 + VK_CAPITAL = 20 + VK_KANA = 0x15 + VK_HANGEUL = 0x15 + VK_HANGUL = 0x15 + VK_JUNJA = 0x17 + VK_FINAL = 0x18 + VK_HANJA = 0x19 + VK_KANJI = 0x19 + VK_ESCAPE = 0x1B + VK_CONVERT = 0x1C + VK_NONCONVERT = 0x1D + VK_ACCEPT = 0x1E + VK_MODECHANGE = 0x1F + VK_SPACE = 32 + VK_PRIOR = 33 + VK_NEXT = 34 + VK_END = 35 + VK_HOME = 36 + VK_LEFT = 37 + VK_UP = 38 + VK_RIGHT = 39 + VK_DOWN = 40 + VK_SELECT = 41 + VK_PRINT = 42 + VK_EXECUTE = 43 + VK_SNAPSHOT = 44 + VK_INSERT = 45 + VK_DELETE = 46 + VK_HELP = 47 + VK_LWIN = 0x5B + VK_RWIN = 0x5C + VK_APPS = 0x5D + VK_SLEEP = 0x5F + VK_NUMPAD0 = 0x60 + VK_NUMPAD1 = 0x61 + VK_NUMPAD2 = 0x62 + VK_NUMPAD3 = 0x63 + VK_NUMPAD4 = 0x64 + VK_NUMPAD5 = 0x65 + VK_NUMPAD6 = 0x66 + VK_NUMPAD7 = 0x67 + VK_NUMPAD8 = 0x68 + VK_NUMPAD9 = 0x69 + VK_MULTIPLY = 0x6A + VK_ADD = 0x6B + VK_SEPARATOR = 0x6C + VK_SUBTRACT = 0x6D + VK_DECIMAL = 0x6E + VK_DIVIDE = 0x6F + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + VK_F13 = 0x7C + VK_F14 = 0x7D + VK_F15 = 0x7E + VK_F16 = 0x7F + VK_F17 = 0x80 + VK_F18 = 0x81 + VK_F19 = 0x82 + VK_F20 = 0x83 + VK_F21 = 0x84 + VK_F22 = 0x85 + VK_F23 = 0x86 + VK_F24 = 0x87 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_LSHIFT = 0xA0 + VK_RSHIFT = 0xA1 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + VK_LMENU = 0xA4 + VK_RMENU = 0xA5 + VK_BROWSER_BACK = 0xA6 + VK_BROWSER_FORWARD = 0xA7 + VK_BROWSER_REFRESH = 0xA8 + VK_BROWSER_STOP = 0xA9 + VK_BROWSER_SEARCH = 0xAA + VK_BROWSER_FAVORITES = 0xAB + VK_BROWSER_HOME = 0xAC + VK_VOLUME_MUTE = 0xAD + VK_VOLUME_DOWN = 0xAE + VK_VOLUME_UP = 0xAF + VK_MEDIA_NEXT_TRACK = 0xB0 + VK_MEDIA_PREV_TRACK = 0xB1 + VK_MEDIA_STOP = 0xB2 + VK_MEDIA_PLAY_PAUSE = 0xB3 + VK_LAUNCH_MAIL = 0xB4 + VK_LAUNCH_MEDIA_SELECT = 0xB5 + VK_LAUNCH_APP1 = 0xB6 + VK_LAUNCH_APP2 = 0xB7 + VK_OEM_1 = 0xBA + VK_OEM_PLUS = 0xBB + VK_OEM_COMMA = 0xBC + VK_OEM_MINUS = 0xBD + VK_OEM_PERIOD = 0xBE + VK_OEM_2 = 0xBF + VK_OEM_3 = 0xC0 + VK_OEM_4 = 0xDB + VK_OEM_5 = 0xDC + VK_OEM_6 = 0xDD + VK_OEM_7 = 0xDE + VK_OEM_8 = 0xDF + VK_OEM_102 = 0xE2 + VK_PROCESSKEY = 0xE5 + VK_PACKET = 0xE7 + VK_ATTN = 0xF6 + VK_CRSEL = 0xF7 + VK_EXSEL = 0xF8 + VK_EREOF = 0xF9 + VK_PLAY = 0xFA + VK_ZOOM = 0xFB + VK_NONAME = 0xFC + VK_PA1 = 0xFD + VK_OEM_CLEAR = 0xFE +) + +const ( + KeyLButton Key = VK_LBUTTON + KeyRButton Key = VK_RBUTTON + KeyCancel Key = VK_CANCEL + KeyMButton Key = VK_MBUTTON + KeyXButton1 Key = VK_XBUTTON1 + KeyXButton2 Key = VK_XBUTTON2 + KeyBack Key = VK_BACK + KeyTab Key = VK_TAB + KeyClear Key = VK_CLEAR + KeyReturn Key = VK_RETURN + KeyShift Key = VK_SHIFT + KeyControl Key = VK_CONTROL + KeyAlt Key = VK_MENU + KeyMenu Key = VK_MENU + KeyPause Key = VK_PAUSE + KeyCapital Key = VK_CAPITAL + KeyKana Key = VK_KANA + KeyHangul Key = VK_HANGUL + KeyJunja Key = VK_JUNJA + KeyFinal Key = VK_FINAL + KeyHanja Key = VK_HANJA + KeyKanji Key = VK_KANJI + KeyEscape Key = VK_ESCAPE + KeyConvert Key = VK_CONVERT + KeyNonconvert Key = VK_NONCONVERT + KeyAccept Key = VK_ACCEPT + KeyModeChange Key = VK_MODECHANGE + KeySpace Key = VK_SPACE + KeyPrior Key = VK_PRIOR + KeyNext Key = VK_NEXT + KeyEnd Key = VK_END + KeyHome Key = VK_HOME + KeyLeft Key = VK_LEFT + KeyUp Key = VK_UP + KeyRight Key = VK_RIGHT + KeyDown Key = VK_DOWN + KeySelect Key = VK_SELECT + KeyPrint Key = VK_PRINT + KeyExecute Key = VK_EXECUTE + KeySnapshot Key = VK_SNAPSHOT + KeyInsert Key = VK_INSERT + KeyDelete Key = VK_DELETE + KeyHelp Key = VK_HELP + Key0 Key = 0x30 + Key1 Key = 0x31 + Key2 Key = 0x32 + Key3 Key = 0x33 + Key4 Key = 0x34 + Key5 Key = 0x35 + Key6 Key = 0x36 + Key7 Key = 0x37 + Key8 Key = 0x38 + Key9 Key = 0x39 + KeyA Key = 0x41 + KeyB Key = 0x42 + KeyC Key = 0x43 + KeyD Key = 0x44 + KeyE Key = 0x45 + KeyF Key = 0x46 + KeyG Key = 0x47 + KeyH Key = 0x48 + KeyI Key = 0x49 + KeyJ Key = 0x4A + KeyK Key = 0x4B + KeyL Key = 0x4C + KeyM Key = 0x4D + KeyN Key = 0x4E + KeyO Key = 0x4F + KeyP Key = 0x50 + KeyQ Key = 0x51 + KeyR Key = 0x52 + KeyS Key = 0x53 + KeyT Key = 0x54 + KeyU Key = 0x55 + KeyV Key = 0x56 + KeyW Key = 0x57 + KeyX Key = 0x58 + KeyY Key = 0x59 + KeyZ Key = 0x5A + KeyLWIN Key = VK_LWIN + KeyRWIN Key = VK_RWIN + KeyApps Key = VK_APPS + KeySleep Key = VK_SLEEP + KeyNumpad0 Key = VK_NUMPAD0 + KeyNumpad1 Key = VK_NUMPAD1 + KeyNumpad2 Key = VK_NUMPAD2 + KeyNumpad3 Key = VK_NUMPAD3 + KeyNumpad4 Key = VK_NUMPAD4 + KeyNumpad5 Key = VK_NUMPAD5 + KeyNumpad6 Key = VK_NUMPAD6 + KeyNumpad7 Key = VK_NUMPAD7 + KeyNumpad8 Key = VK_NUMPAD8 + KeyNumpad9 Key = VK_NUMPAD9 + KeyMultiply Key = VK_MULTIPLY + KeyAdd Key = VK_ADD + KeySeparator Key = VK_SEPARATOR + KeySubtract Key = VK_SUBTRACT + KeyDecimal Key = VK_DECIMAL + KeyDivide Key = VK_DIVIDE + KeyF1 Key = VK_F1 + KeyF2 Key = VK_F2 + KeyF3 Key = VK_F3 + KeyF4 Key = VK_F4 + KeyF5 Key = VK_F5 + KeyF6 Key = VK_F6 + KeyF7 Key = VK_F7 + KeyF8 Key = VK_F8 + KeyF9 Key = VK_F9 + KeyF10 Key = VK_F10 + KeyF11 Key = VK_F11 + KeyF12 Key = VK_F12 + KeyF13 Key = VK_F13 + KeyF14 Key = VK_F14 + KeyF15 Key = VK_F15 + KeyF16 Key = VK_F16 + KeyF17 Key = VK_F17 + KeyF18 Key = VK_F18 + KeyF19 Key = VK_F19 + KeyF20 Key = VK_F20 + KeyF21 Key = VK_F21 + KeyF22 Key = VK_F22 + KeyF23 Key = VK_F23 + KeyF24 Key = VK_F24 + KeyNumlock Key = VK_NUMLOCK + KeyScroll Key = VK_SCROLL + KeyLShift Key = VK_LSHIFT + KeyRShift Key = VK_RSHIFT + KeyLControl Key = VK_LCONTROL + KeyRControl Key = VK_RCONTROL + KeyLAlt Key = VK_LMENU + KeyLMenu Key = VK_LMENU + KeyRAlt Key = VK_RMENU + KeyRMenu Key = VK_RMENU + KeyBrowserBack Key = VK_BROWSER_BACK + KeyBrowserForward Key = VK_BROWSER_FORWARD + KeyBrowserRefresh Key = VK_BROWSER_REFRESH + KeyBrowserStop Key = VK_BROWSER_STOP + KeyBrowserSearch Key = VK_BROWSER_SEARCH + KeyBrowserFavorites Key = VK_BROWSER_FAVORITES + KeyBrowserHome Key = VK_BROWSER_HOME + KeyVolumeMute Key = VK_VOLUME_MUTE + KeyVolumeDown Key = VK_VOLUME_DOWN + KeyVolumeUp Key = VK_VOLUME_UP + KeyMediaNextTrack Key = VK_MEDIA_NEXT_TRACK + KeyMediaPrevTrack Key = VK_MEDIA_PREV_TRACK + KeyMediaStop Key = VK_MEDIA_STOP + KeyMediaPlayPause Key = VK_MEDIA_PLAY_PAUSE + KeyLaunchMail Key = VK_LAUNCH_MAIL + KeyLaunchMediaSelect Key = VK_LAUNCH_MEDIA_SELECT + KeyLaunchApp1 Key = VK_LAUNCH_APP1 + KeyLaunchApp2 Key = VK_LAUNCH_APP2 + KeyOEM1 Key = VK_OEM_1 + KeyOEMPlus Key = VK_OEM_PLUS + KeyOEMComma Key = VK_OEM_COMMA + KeyOEMMinus Key = VK_OEM_MINUS + KeyOEMPeriod Key = VK_OEM_PERIOD + KeyOEM2 Key = VK_OEM_2 + KeyOEM3 Key = VK_OEM_3 + KeyOEM4 Key = VK_OEM_4 + KeyOEM5 Key = VK_OEM_5 + KeyOEM6 Key = VK_OEM_6 + KeyOEM7 Key = VK_OEM_7 + KeyOEM8 Key = VK_OEM_8 + KeyOEM102 Key = VK_OEM_102 + KeyProcessKey Key = VK_PROCESSKEY + KeyPacket Key = VK_PACKET + KeyAttn Key = VK_ATTN + KeyCRSel Key = VK_CRSEL + KeyEXSel Key = VK_EXSEL + KeyErEOF Key = VK_EREOF + KeyPlay Key = VK_PLAY + KeyZoom Key = VK_ZOOM + KeyNoName Key = VK_NONAME + KeyPA1 Key = VK_PA1 + KeyOEMClear Key = VK_OEM_CLEAR +) + +var key2string = map[Key]string{ + KeyLButton: "LButton", + KeyRButton: "RButton", + KeyCancel: "Cancel", + KeyMButton: "MButton", + KeyXButton1: "XButton1", + KeyXButton2: "XButton2", + KeyBack: "Back", + KeyTab: "Tab", + KeyClear: "Clear", + KeyReturn: "Return", + KeyShift: "Shift", + KeyControl: "Control", + KeyAlt: "Alt / Menu", + KeyPause: "Pause", + KeyCapital: "Capital", + KeyKana: "Kana / Hangul", + KeyJunja: "Junja", + KeyFinal: "Final", + KeyHanja: "Hanja / Kanji", + KeyEscape: "Escape", + KeyConvert: "Convert", + KeyNonconvert: "Nonconvert", + KeyAccept: "Accept", + KeyModeChange: "ModeChange", + KeySpace: "Space", + KeyPrior: "Prior", + KeyNext: "Next", + KeyEnd: "End", + KeyHome: "Home", + KeyLeft: "Left", + KeyUp: "Up", + KeyRight: "Right", + KeyDown: "Down", + KeySelect: "Select", + KeyPrint: "Print", + KeyExecute: "Execute", + KeySnapshot: "Snapshot", + KeyInsert: "Insert", + KeyDelete: "Delete", + KeyHelp: "Help", + Key0: "0", + Key1: "1", + Key2: "2", + Key3: "3", + Key4: "4", + Key5: "5", + Key6: "6", + Key7: "7", + Key8: "8", + Key9: "9", + KeyA: "A", + KeyB: "B", + KeyC: "C", + KeyD: "D", + KeyE: "E", + KeyF: "F", + KeyG: "G", + KeyH: "H", + KeyI: "I", + KeyJ: "J", + KeyK: "K", + KeyL: "L", + KeyM: "M", + KeyN: "N", + KeyO: "O", + KeyP: "P", + KeyQ: "Q", + KeyR: "R", + KeyS: "S", + KeyT: "T", + KeyU: "U", + KeyV: "V", + KeyW: "W", + KeyX: "X", + KeyY: "Y", + KeyZ: "Z", + KeyLWIN: "LWIN", + KeyRWIN: "RWIN", + KeyApps: "Apps", + KeySleep: "Sleep", + KeyNumpad0: "Numpad0", + KeyNumpad1: "Numpad1", + KeyNumpad2: "Numpad2", + KeyNumpad3: "Numpad3", + KeyNumpad4: "Numpad4", + KeyNumpad5: "Numpad5", + KeyNumpad6: "Numpad6", + KeyNumpad7: "Numpad7", + KeyNumpad8: "Numpad8", + KeyNumpad9: "Numpad9", + KeyMultiply: "Multiply", + KeyAdd: "Add", + KeySeparator: "Separator", + KeySubtract: "Subtract", + KeyDecimal: "Decimal", + KeyDivide: "Divide", + KeyF1: "F1", + KeyF2: "F2", + KeyF3: "F3", + KeyF4: "F4", + KeyF5: "F5", + KeyF6: "F6", + KeyF7: "F7", + KeyF8: "F8", + KeyF9: "F9", + KeyF10: "F10", + KeyF11: "F11", + KeyF12: "F12", + KeyF13: "F13", + KeyF14: "F14", + KeyF15: "F15", + KeyF16: "F16", + KeyF17: "F17", + KeyF18: "F18", + KeyF19: "F19", + KeyF20: "F20", + KeyF21: "F21", + KeyF22: "F22", + KeyF23: "F23", + KeyF24: "F24", + KeyNumlock: "Numlock", + KeyScroll: "Scroll", + KeyLShift: "LShift", + KeyRShift: "RShift", + KeyLControl: "LControl", + KeyRControl: "RControl", + KeyLMenu: "LMenu", + KeyRMenu: "RMenu", + KeyBrowserBack: "BrowserBack", + KeyBrowserForward: "BrowserForward", + KeyBrowserRefresh: "BrowserRefresh", + KeyBrowserStop: "BrowserStop", + KeyBrowserSearch: "BrowserSearch", + KeyBrowserFavorites: "BrowserFavorites", + KeyBrowserHome: "BrowserHome", + KeyVolumeMute: "VolumeMute", + KeyVolumeDown: "VolumeDown", + KeyVolumeUp: "VolumeUp", + KeyMediaNextTrack: "MediaNextTrack", + KeyMediaPrevTrack: "MediaPrevTrack", + KeyMediaStop: "MediaStop", + KeyMediaPlayPause: "MediaPlayPause", + KeyLaunchMail: "LaunchMail", + KeyLaunchMediaSelect: "LaunchMediaSelect", + KeyLaunchApp1: "LaunchApp1", + KeyLaunchApp2: "LaunchApp2", + KeyOEM1: "OEM1", + KeyOEMPlus: "OEMPlus", + KeyOEMComma: "OEMComma", + KeyOEMMinus: "OEMMinus", + KeyOEMPeriod: "OEMPeriod", + KeyOEM2: "OEM2", + KeyOEM3: "OEM3", + KeyOEM4: "OEM4", + KeyOEM5: "OEM5", + KeyOEM6: "OEM6", + KeyOEM7: "OEM7", + KeyOEM8: "OEM8", + KeyOEM102: "OEM102", + KeyProcessKey: "ProcessKey", + KeyPacket: "Packet", + KeyAttn: "Attn", + KeyCRSel: "CRSel", + KeyEXSel: "EXSel", + KeyErEOF: "ErEOF", + KeyPlay: "Play", + KeyZoom: "Zoom", + KeyNoName: "NoName", + KeyPA1: "PA1", + KeyOEMClear: "OEMClear", +} + +type Modifiers byte + +func (m Modifiers) String() string { + return modifiers2string[m] +} + +var modifiers2string = map[Modifiers]string{ + ModShift: "Shift", + ModControl: "Ctrl", + ModControl | ModShift: "Ctrl+Shift", + ModAlt: "Alt", + ModAlt | ModShift: "Alt+Shift", + ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift", +} + +const ( + ModShift Modifiers = 1 << iota + ModControl + ModAlt +) + +func ModifiersDown() Modifiers { + var m Modifiers + + if ShiftDown() { + m |= ModShift + } + if ControlDown() { + m |= ModControl + } + if AltDown() { + m |= ModAlt + } + + return m +} + +type Shortcut struct { + Modifiers Modifiers + Key Key +} + +func (s Shortcut) String() string { + m := s.Modifiers.String() + if m == "" { + return s.Key.String() + } + + b := new(bytes.Buffer) + + b.WriteString(m) + b.WriteRune('+') + b.WriteString(s.Key.String()) + + return b.String() +} + +func GetKeyState(nVirtKey int32) int16 { + ret, _, _ := procGetKeyState.Call( + uintptr(nVirtKey), + ) + + return int16(ret) +} + +func AltDown() bool { + return GetKeyState(int32(KeyAlt))>>15 != 0 +} + +func ControlDown() bool { + return GetKeyState(int32(KeyControl))>>15 != 0 +} + +func ShiftDown() bool { + return GetKeyState(int32(KeyShift))>>15 != 0 +} + +var ModifierMap = map[keys.Modifier]Modifiers{ + keys.ShiftKey: ModShift, + keys.ControlKey: ModControl, + keys.OptionOrAltKey: ModAlt, + keys.CmdOrCtrlKey: ModControl, +} + +var NoShortcut = Shortcut{} + +func AcceleratorToShortcut(accelerator *keys.Accelerator) Shortcut { + + if accelerator == nil { + return NoShortcut + } + inKey := strings.ToUpper(accelerator.Key) + key, exists := KeyMap[inKey] + if !exists { + return NoShortcut + } + var modifiers Modifiers + if _, exists := shiftMap[inKey]; exists { + modifiers = ModShift + } + for _, mod := range accelerator.Modifiers { + modifiers |= ModifierMap[mod] + } + return Shortcut{ + Modifiers: modifiers, + Key: key, + } +} + +var shiftMap = map[string]struct{}{ + "~": {}, + ")": {}, + "!": {}, + "@": {}, + "#": {}, + "$": {}, + "%": {}, + "^": {}, + "&": {}, + "*": {}, + "(": {}, + "_": {}, + "PLUS": {}, + "<": {}, + ">": {}, + "?": {}, + ":": {}, + `"`: {}, + "{": {}, + "}": {}, + "|": {}, +} + +var KeyMap = map[string]Key{ + "0": Key0, + "1": Key1, + "2": Key2, + "3": Key3, + "4": Key4, + "5": Key5, + "6": Key6, + "7": Key7, + "8": Key8, + "9": Key9, + "A": KeyA, + "B": KeyB, + "C": KeyC, + "D": KeyD, + "E": KeyE, + "F": KeyF, + "G": KeyG, + "H": KeyH, + "I": KeyI, + "J": KeyJ, + "K": KeyK, + "L": KeyL, + "M": KeyM, + "N": KeyN, + "O": KeyO, + "P": KeyP, + "Q": KeyQ, + "R": KeyR, + "S": KeyS, + "T": KeyT, + "U": KeyU, + "V": KeyV, + "W": KeyW, + "X": KeyX, + "Y": KeyY, + "Z": KeyZ, + "F1": KeyF1, + "F2": KeyF2, + "F3": KeyF3, + "F4": KeyF4, + "F5": KeyF5, + "F6": KeyF6, + "F7": KeyF7, + "F8": KeyF8, + "F9": KeyF9, + "F10": KeyF10, + "F11": KeyF11, + "F12": KeyF12, + "F13": KeyF13, + "F14": KeyF14, + "F15": KeyF15, + "F16": KeyF16, + "F17": KeyF17, + "F18": KeyF18, + "F19": KeyF19, + "F20": KeyF20, + "F21": KeyF21, + "F22": KeyF22, + "F23": KeyF23, + "F24": KeyF24, + + "`": KeyOEM3, + ",": KeyOEMComma, + ".": KeyOEMPeriod, + "/": KeyOEM2, + ";": KeyOEM1, + "'": KeyOEM7, + "[": KeyOEM4, + "]": KeyOEM6, + `\`: KeyOEM5, + "~": KeyOEM3, + ")": Key0, + "!": Key1, + "@": Key2, + "#": Key3, + "$": Key4, + "%": Key5, + "^": Key6, + "&": Key7, + "*": Key8, + "(": Key9, + "_": KeyOEMMinus, + "PLUS": KeyOEMPlus, + "<": KeyOEMComma, + ">": KeyOEMPeriod, + "?": KeyOEM2, + ":": KeyOEM1, + `"`: KeyOEM7, + "{": KeyOEM4, + "}": KeyOEM6, + "|": KeyOEM5, + + "SPACE": KeySpace, + "TAB": KeyTab, + "CAPSLOCK": KeyCapital, + "NUMLOCK": KeyNumlock, + "SCROLLLOCK": KeyScroll, + "BACKSPACE": KeyBack, + "DELETE": KeyDelete, + "INSERT": KeyInsert, + "RETURN": KeyReturn, + "ENTER": KeyReturn, + "UP": KeyUp, + "DOWN": KeyDown, + "LEFT": KeyLeft, + "RIGHT": KeyRight, + "HOME": KeyHome, + "END": KeyEnd, + "PAGEUP": KeyPrior, + "PAGEDOWN": KeyNext, + "ESCAPE": KeyEscape, + "ESC": KeyEscape, + "VOLUMEUP": KeyVolumeUp, + "VOLUMEDOWN": KeyVolumeDown, + "VOLUMEMUTE": KeyVolumeMute, + "MEDIANEXTTRACK": KeyMediaNextTrack, + "MEDIAPREVIOUSTRACK": KeyMediaPrevTrack, + "MEDIASTOP": KeyMediaStop, + "MEDIAPLAYPAUSE": KeyMediaPlayPause, + "PRINTSCREEN": KeyPrint, + "NUM0": KeyNumpad0, + "NUM1": KeyNumpad1, + "NUM2": KeyNumpad2, + "NUM3": KeyNumpad3, + "NUM4": KeyNumpad4, + "NUM5": KeyNumpad5, + "NUM6": KeyNumpad6, + "NUM7": KeyNumpad7, + "NUM8": KeyNumpad8, + "NUM9": KeyNumpad9, + "nummult": KeyMultiply, + "numadd": KeyAdd, + "numsub": KeySubtract, + "numdec": KeyDecimal, + "numdiv": KeyDivide, +} + +type Accelerator struct { + Virtual byte + Key uint16 + Cmd uint16 +} + +func CreateAcceleratorTable(acc []Accelerator) uintptr { + if len(acc) == 0 { + return 0 + } + ret, _, _ := procCreateAcceleratorTable.Call( + uintptr(unsafe.Pointer(&acc[0])), + uintptr(len(acc)), + ) + return ret +} + +func TranslateAccelerator(hwnd HWND, hAccTable uintptr, lpMsg *MSG) bool { + ret, _, _ := procTranslateAccelerator.Call( + uintptr(hwnd), + hAccTable, + uintptr(unsafe.Pointer(lpMsg)), + ) + return ret != 0 +} diff --git a/v2/internal/platform/win32/menu.go b/v2/internal/platform/win32/menu.go new file mode 100644 index 000000000..f05886414 --- /dev/null +++ b/v2/internal/platform/win32/menu.go @@ -0,0 +1,82 @@ +//go:build windows + +package win32 + +type Menu HMENU +type PopupMenu Menu + +func CreatePopupMenu() PopupMenu { + ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0) + return PopupMenu(ret) +} + +func (m Menu) Destroy() bool { + ret, _, _ := procDestroyMenu.Call(uintptr(m)) + return ret != 0 +} + +func (p PopupMenu) Destroy() bool { + return Menu(p).Destroy() +} + +func (p PopupMenu) Track(flags uint, x, y int, wnd HWND) bool { + ret, _, _ := procTrackPopupMenu.Call( + uintptr(p), + uintptr(flags), + uintptr(x), + uintptr(y), + 0, + uintptr(wnd), + 0, + ) + return ret != 0 +} + +func (p PopupMenu) Append(flags uintptr, id uintptr, text string) bool { + return Menu(p).Append(flags, id, text) +} + +func (m Menu) Append(flags uintptr, id uintptr, text string) bool { + ret, _, _ := procAppendMenuW.Call( + uintptr(m), + flags, + id, + MustStringToUTF16uintptr(text), + ) + return ret != 0 +} + +func (p PopupMenu) Check(id uintptr, checked bool) bool { + return Menu(p).Check(id, checked) +} + +func (m Menu) Check(id uintptr, check bool) bool { + var checkState uint = MF_UNCHECKED + if check { + checkState = MF_CHECKED + } + return CheckMenuItem(HMENU(m), id, checkState) != 0 +} + +func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool { + ret, _, _ := procCheckMenuRadioItem.Call( + uintptr(m), + uintptr(startID), + uintptr(endID), + uintptr(selectedID), + MF_BYCOMMAND) + return ret != 0 +} + +func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint { + ret, _, _ := procCheckMenuItem.Call( + uintptr(menu), + id, + uintptr(flags), + ) + return uint(ret) +} + +func (p PopupMenu) CheckRadio(startID, endID, selectedID int) bool { + return Menu(p).CheckRadio(startID, endID, selectedID) +} diff --git a/v2/internal/platform/win32/structs.go b/v2/internal/platform/win32/structs.go new file mode 100644 index 000000000..3f79d8585 --- /dev/null +++ b/v2/internal/platform/win32/structs.go @@ -0,0 +1,51 @@ +//go:build windows + +package win32 + +import "golang.org/x/sys/windows" + +type NOTIFYICONDATA struct { + CbSize uint32 + HWnd HWND + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon HICON + SzTip [128]uint16 + DwState uint32 + DwStateMask uint32 + SzInfo [256]uint16 + UVersion uint32 + SzInfoTitle [64]uint16 + DwInfoFlags uint32 + GuidItem windows.GUID + HBalloonIcon HICON +} + +type WNDCLASSEX struct { + CbSize uint32 + Style uint32 + LpfnWndProc uintptr + CbClsExtra int32 + CbWndExtra int32 + HInstance HINSTANCE + HIcon HICON + HCursor HCURSOR + HbrBackground HBRUSH + LpszMenuName *uint16 + LpszClassName *uint16 + HIconSm HICON +} + +type MSG struct { + HWnd HWND + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt POINT +} + +type POINT struct { + X, Y int32 +} diff --git a/v2/internal/platform/win32/theme.go b/v2/internal/platform/win32/theme.go new file mode 100644 index 000000000..ad29b1201 --- /dev/null +++ b/v2/internal/platform/win32/theme.go @@ -0,0 +1,191 @@ +//go:build windows + +package win32 + +import ( + "golang.org/x/sys/windows/registry" + "unsafe" +) + +type DWMWINDOWATTRIBUTE int32 + +const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19 +const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20 +const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34 +const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35 +const DwmwaTextColor DWMWINDOWATTRIBUTE = 36 +const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38 + +const SPI_GETHIGHCONTRAST = 0x0042 +const HCF_HIGHCONTRASTON = 0x00000001 +const WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19 + +type ACCENT_STATE DWORD + +const ( + ACCENT_DISABLED ACCENT_STATE = 0 + ACCENT_ENABLE_GRADIENT ACCENT_STATE = 1 + ACCENT_ENABLE_TRANSPARENTGRADIENT ACCENT_STATE = 2 + ACCENT_ENABLE_BLURBEHIND ACCENT_STATE = 3 + ACCENT_ENABLE_ACRYLICBLURBEHIND ACCENT_STATE = 4 // RS4 1803 + ACCENT_ENABLE_HOSTBACKDROP ACCENT_STATE = 5 // RS5 1809 + ACCENT_INVALID_STATE ACCENT_STATE = 6 +) + +type ACCENT_POLICY struct { + AccentState ACCENT_STATE + AccentFlags DWORD + GradientColor DWORD + AnimationId DWORD +} + +type WINDOWCOMPOSITIONATTRIBDATA struct { + Attrib WINDOWCOMPOSITIONATTRIB + PvData unsafe.Pointer + CbData uintptr +} + +type WINDOWCOMPOSITIONATTRIB DWORD + +// BackdropType defines the type of translucency we wish to use +type BackdropType int32 + +const ( + BackdropTypeAuto BackdropType = 0 + BackdropTypeNone BackdropType = 1 + BackdropTypeMica BackdropType = 2 + BackdropTypeAcrylic BackdropType = 3 + BackdropTypeTabbed BackdropType = 4 +) + +func dwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) { + ret, _, err := procDwmSetWindowAttribute.Call( + uintptr(hwnd), + uintptr(dwAttribute), + uintptr(pvAttribute), + cbAttribute) + if ret != 0 { + _ = err + // println(err.Error()) + } +} + +func SupportsThemes() bool { + // We can't support Windows versions before 17763 + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsCustomThemes() bool { + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsBackdropTypes() bool { + return IsWindowsVersionAtLeast(10, 0, 22621) +} + +func SupportsImmersiveDarkMode() bool { + return IsWindowsVersionAtLeast(10, 0, 18985) +} + +func SetTheme(hwnd HWND, useDarkMode bool) { + if SupportsThemes() { + attr := DwmwaUseImmersiveDarkModeBefore20h1 + if SupportsImmersiveDarkMode() { + attr = DwmwaUseImmersiveDarkMode + } + var winDark int32 + if useDarkMode { + winDark = 1 + } + dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark)) + } +} + +func EnableBlurBehind(hwnd HWND) { + var accent = ACCENT_POLICY{ + AccentState: ACCENT_ENABLE_ACRYLICBLURBEHIND, + AccentFlags: 0x2, + } + var data WINDOWCOMPOSITIONATTRIBDATA + data.Attrib = WCA_ACCENT_POLICY + data.PvData = unsafe.Pointer(&accent) + data.CbData = unsafe.Sizeof(accent) + + SetWindowCompositionAttribute(hwnd, &data) +} + +func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool { + if procSetWindowCompositionAttribute != nil { + ret, _, _ := procSetWindowCompositionAttribute.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(data)), + ) + return ret != 0 + } + return false +} + +func EnableTranslucency(hwnd HWND, backdrop BackdropType) { + if SupportsBackdropTypes() { + dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop)) + } else { + println("Warning: Translucency type unavailable on Windows < 22621") + } +} + +func SetTitleBarColour(hwnd HWND, titleBarColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour)) +} + +func SetTitleTextColour(hwnd HWND, titleTextColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour)) +} + +func SetBorderColour(hwnd HWND, titleBorderColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour)) +} + +func SetWindowTheme(hwnd HWND, appName string, subIdList string) uintptr { + var subID uintptr + if subIdList != "" { + subID = MustStringToUTF16uintptr(subIdList) + } + ret, _, _ := procSetWindowTheme.Call( + uintptr(hwnd), + MustStringToUTF16uintptr(appName), + subID, + ) + + return ret +} +func IsCurrentlyDarkMode() bool { + key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + + AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme") + if err != nil { + return false + } + return AppsUseLightTheme == 0 +} + +type highContrast struct { + CbSize uint32 + DwFlags uint32 + LpszDefaultScheme *int16 +} + +func IsCurrentlyHighContrastMode() bool { + var result highContrast + result.CbSize = uint32(unsafe.Sizeof(result)) + res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0) + if res == 0 { + _ = err + return false + } + r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON + return r +} diff --git a/v2/internal/platform/win32/window.go b/v2/internal/platform/win32/window.go new file mode 100644 index 000000000..0ca31ecee --- /dev/null +++ b/v2/internal/platform/win32/window.go @@ -0,0 +1,139 @@ +//go:build windows + +package win32 + +import ( + "fmt" + "github.com/samber/lo" + "golang.org/x/sys/windows" + "syscall" + "unsafe" +) + +func LoadIconWithResourceID(instance HINSTANCE, res uintptr) HICON { + ret, _, _ := procLoadIcon.Call( + uintptr(instance), + res) + + return HICON(ret) +} + +func LoadCursorWithResourceID(instance HINSTANCE, res uintptr) HCURSOR { + ret, _, _ := procLoadCursor.Call( + uintptr(instance), + res) + + return HCURSOR(ret) +} + +func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { + ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx))) + return ATOM(ret) +} + +func RegisterClass(className string, wndproc uintptr, instance HINSTANCE) error { + classNamePtr, err := syscall.UTF16PtrFromString(className) + if err != nil { + return err + } + icon := LoadIconWithResourceID(instance, IDI_APPLICATION) + + var wc WNDCLASSEX + wc.CbSize = uint32(unsafe.Sizeof(wc)) + wc.Style = CS_HREDRAW | CS_VREDRAW + wc.LpfnWndProc = wndproc + wc.HInstance = instance + wc.HbrBackground = COLOR_WINDOW + 1 + wc.HIcon = icon + wc.HCursor = LoadCursorWithResourceID(0, IDC_ARROW) + wc.LpszClassName = classNamePtr + wc.LpszMenuName = nil + wc.HIconSm = icon + + if ret := RegisterClassEx(&wc); ret == 0 { + return syscall.GetLastError() + } + + return nil +} + +func CreateWindow(className string, instance HINSTANCE, parent HWND, exStyle, style uint) HWND { + + classNamePtr := lo.Must(syscall.UTF16PtrFromString(className)) + + result := CreateWindowEx( + exStyle, + classNamePtr, + nil, + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent, + 0, + instance, + nil) + + if result == 0 { + errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style) + panic(errStr) + } + + return result +} + +func CreateWindowEx(exStyle uint, className, windowName *uint16, + style uint, x, y, width, height int, parent HWND, menu HMENU, + instance HINSTANCE, param unsafe.Pointer) HWND { + ret, _, _ := procCreateWindowEx.Call( + uintptr(exStyle), + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName)), + uintptr(style), + uintptr(x), + uintptr(y), + uintptr(width), + uintptr(height), + uintptr(parent), + uintptr(menu), + uintptr(instance), + uintptr(param)) + + return HWND(ret) +} + +func MustStringToUTF16Ptr(input string) *uint16 { + ret, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return ret +} + +func MustStringToUTF16uintptr(input string) uintptr { + ret, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return uintptr(unsafe.Pointer(ret)) +} + +func MustUTF16FromString(input string) []uint16 { + ret, err := syscall.UTF16FromString(input) + if err != nil { + panic(err) + } + return ret +} + +func UTF16PtrToString(input uintptr) string { + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(input))) +} + +func SetForegroundWindow(wnd HWND) bool { + ret, _, _ := procSetForegroundWindow.Call( + uintptr(wnd), + ) + return ret != 0 +} diff --git a/v2/internal/process/process.go b/v2/internal/process/process.go new file mode 100644 index 000000000..18c9f45da --- /dev/null +++ b/v2/internal/process/process.go @@ -0,0 +1,74 @@ +package process + +import ( + "os" + "os/exec" +) + +// Process defines a process that can be executed +type Process struct { + cmd *exec.Cmd + exitChannel chan bool + Running bool +} + +// NewProcess creates a new process struct +func NewProcess(cmd string, args ...string) *Process { + result := &Process{ + cmd: exec.Command(cmd, args...), + exitChannel: make(chan bool, 1), + } + result.cmd.Stdout = os.Stdout + result.cmd.Stderr = os.Stderr + return result +} + +// Start the process +func (p *Process) Start(exitCodeChannel chan int) error { + err := p.cmd.Start() + if err != nil { + return err + } + + p.Running = true + + go func(cmd *exec.Cmd, running *bool, exitChannel chan bool, exitCodeChannel chan int) { + err := cmd.Wait() + if err == nil { + exitCodeChannel <- 0 + } + *running = false + exitChannel <- true + }(p.cmd, &p.Running, p.exitChannel, exitCodeChannel) + + return nil +} + +// Kill the process +func (p *Process) Kill() error { + if !p.Running { + return nil + } + err := p.cmd.Process.Kill() + if err != nil { + return err + } + err = p.cmd.Process.Release() + if err != nil { + return err + } + + // Wait for command to exit properly + <-p.exitChannel + + return err +} + +// PID returns the process PID +func (p *Process) PID() int { + return p.cmd.Process.Pid +} + +func (p *Process) SetDir(dir string) { + p.cmd.Dir = dir +} diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go new file mode 100644 index 000000000..2df99bdfa --- /dev/null +++ b/v2/internal/project/project.go @@ -0,0 +1,283 @@ +package project + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/samber/lo" +) + +// Project holds the data related to a Wails project +type Project struct { + /*** Application Data ***/ + Name string `json:"name"` + AssetDirectory string `json:"assetdir,omitempty"` + + ReloadDirectories string `json:"reloaddirs,omitempty"` + + BuildCommand string `json:"frontend:build"` + InstallCommand string `json:"frontend:install"` + + // Commands used in `wails dev` + DevCommand string `json:"frontend:dev"` + DevBuildCommand string `json:"frontend:dev:build"` + DevInstallCommand string `json:"frontend:dev:install"` + DevWatcherCommand string `json:"frontend:dev:watcher"` + // The url of the external wails dev server. If this is set, this server is used for the frontend. Default "" + FrontendDevServerURL string `json:"frontend:dev:serverUrl"` + + // Directory to generate the API Module + WailsJSDir string `json:"wailsjsdir"` + + Version string `json:"version"` + + /*** Internal Data ***/ + + // The path to the project directory + Path string `json:"projectdir"` + + // Build directory + BuildDir string `json:"build:dir"` + + // BuildTags Extra tags to process during build + BuildTags string `json:"build:tags"` + + // The output filename + OutputFilename string `json:"outputfilename"` + + // The type of application. EG: Desktop, Server, etc + OutputType string + + // The platform to target + Platform string + + // RunNonNativeBuildHooks will run build hooks though they are defined for a GOOS which is not equal to the host os + RunNonNativeBuildHooks bool `json:"runNonNativeBuildHooks"` + + // Build hooks for different targets, the hooks are executed in the following order + // Key: GOOS/GOARCH - Executed at build level before/after a build of the specific platform and arch + // Key: GOOS/* - Executed at build level before/after a build of the specific platform + // Key: */* - Executed at build level before/after a build + // The following keys are not yet supported. + // Key: GOOS - Executed at platform level before/after all builds of the specific platform + // Key: * - Executed at platform level before/after all builds of a platform + // Key: [empty] - Executed at global level before/after all builds of all platforms + PostBuildHooks map[string]string `json:"postBuildHooks"` + PreBuildHooks map[string]string `json:"preBuildHooks"` + + // The application author + Author Author + + // The application information + Info Info + + // Fully qualified filename + filename string + + // The debounce time for hot-reload of the built-in dev server. Default 100 + DebounceMS int `json:"debounceMS"` + + // The address to bind the wails dev server to. Default "localhost:34115" + DevServer string `json:"devServer"` + + // Arguments that are forward to the application in dev mode + AppArgs string `json:"appargs"` + + // NSISType to be build + NSISType string `json:"nsisType"` + + // Garble + Obfuscated bool `json:"obfuscated"` + GarbleArgs string `json:"garbleargs"` + + // Frontend directory + FrontendDir string `json:"frontend:dir"` + + // The timeout in seconds for Vite server detection. Default 10 + ViteServerTimeout int `json:"viteServerTimeout"` + + Bindings Bindings `json:"bindings"` +} + +func (p *Project) GetFrontendDir() string { + if filepath.IsAbs(p.FrontendDir) { + return p.FrontendDir + } + return filepath.Join(p.Path, p.FrontendDir) +} + +func (p *Project) GetWailsJSDir() string { + if filepath.IsAbs(p.WailsJSDir) { + return p.WailsJSDir + } + return filepath.Join(p.Path, p.WailsJSDir) +} + +func (p *Project) GetBuildDir() string { + if filepath.IsAbs(p.BuildDir) { + return p.BuildDir + } + return filepath.Join(p.Path, p.BuildDir) +} + +func (p *Project) GetDevBuildCommand() string { + if p.DevBuildCommand != "" { + return p.DevBuildCommand + } + if p.DevCommand != "" { + return p.DevCommand + } + return p.BuildCommand +} + +func (p *Project) GetDevInstallerCommand() string { + if p.DevInstallCommand != "" { + return p.DevInstallCommand + } + return p.InstallCommand +} + +func (p *Project) IsFrontendDevServerURLAutoDiscovery() bool { + return p.FrontendDevServerURL == "auto" +} + +func (p *Project) Save() error { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return err + } + return os.WriteFile(p.filename, data, 0o755) +} + +func (p *Project) setDefaults() { + if p.Path == "" { + p.Path = lo.Must(os.Getwd()) + } + if p.Version == "" { + p.Version = "2" + } + // Create default name if not given + if p.Name == "" { + p.Name = "wailsapp" + } + if p.OutputFilename == "" { + p.OutputFilename = p.Name + } + if p.FrontendDir == "" { + p.FrontendDir = "frontend" + } + if p.WailsJSDir == "" { + p.WailsJSDir = p.FrontendDir + } + if p.BuildDir == "" { + p.BuildDir = "build" + } + if p.DebounceMS == 0 { + p.DebounceMS = 100 + } + if p.DevServer == "" { + p.DevServer = "localhost:34115" + } + if p.ViteServerTimeout == 0 { + p.ViteServerTimeout = 10 + } + if p.NSISType == "" { + p.NSISType = "multiple" + } + if p.Info.CompanyName == "" { + p.Info.CompanyName = p.Name + } + if p.Info.ProductName == "" { + p.Info.ProductName = p.Name + } + if p.Info.ProductVersion == "" { + p.Info.ProductVersion = "1.0.0" + } + if p.Info.Copyright == nil { + v := "Copyright........." + p.Info.Copyright = &v + } + if p.Info.Comments == nil { + v := "Built using Wails (https://wails.io)" + p.Info.Comments = &v + } + + // Fix up OutputFilename + switch runtime.GOOS { + case "windows": + if !strings.HasSuffix(p.OutputFilename, ".exe") { + p.OutputFilename += ".exe" + } + case "darwin", "linux": + p.OutputFilename = strings.TrimSuffix(p.OutputFilename, ".exe") + } +} + +// Author stores details about the application author +type Author struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type Info struct { + CompanyName string `json:"companyName"` + ProductName string `json:"productName"` + ProductVersion string `json:"productVersion"` + Copyright *string `json:"copyright"` + Comments *string `json:"comments"` + FileAssociations []FileAssociation `json:"fileAssociations"` + Protocols []Protocol `json:"protocols"` +} + +type FileAssociation struct { + Ext string `json:"ext"` + Name string `json:"name"` + Description string `json:"description"` + IconName string `json:"iconName"` + Role string `json:"role"` +} + +type Protocol struct { + Scheme string `json:"scheme"` + Description string `json:"description"` + Role string `json:"role"` +} + +type Bindings struct { + TsGeneration TsGeneration `json:"ts_generation"` +} + +type TsGeneration struct { + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` + OutputType string `json:"outputType"` +} + +// Parse the given JSON data into a Project struct +func Parse(projectData []byte) (*Project, error) { + project := &Project{} + err := json.Unmarshal(projectData, project) + if err != nil { + return nil, err + } + project.setDefaults() + return project, nil +} + +// Load the project from the current working directory +func Load(projectPath string) (*Project, error) { + projectFile := filepath.Join(projectPath, "wails.json") + rawBytes, err := os.ReadFile(projectFile) + if err != nil { + return nil, err + } + result, err := Parse(rawBytes) + if err != nil { + return nil, err + } + result.filename = projectFile + return result, nil +} diff --git a/v2/internal/project/project_test.go b/v2/internal/project/project_test.go new file mode 100644 index 000000000..8c080307b --- /dev/null +++ b/v2/internal/project/project_test.go @@ -0,0 +1,142 @@ +package project_test + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/project" +) + +func TestProject_GetFrontendDir(t *testing.T) { + cwd := lo.Must(os.Getwd()) + tests := []struct { + name string + inputJSON string + want string + wantError bool + }{ + { + name: "Should use 'frontend' by default", + inputJSON: "{}", + want: filepath.ToSlash(filepath.Join(cwd, "frontend")), + wantError: false, + }, + { + name: "Should resolve a relative path with no project path", + inputJSON: `{"frontend:dir": "./frontend"}`, + want: filepath.ToSlash(filepath.Join(cwd, "frontend")), + wantError: false, + }, + { + name: "Should resolve a relative path with project path set", + inputJSON: func() string { + if runtime.GOOS == "windows" { + return `{"frontend:dir": "./frontend", "projectdir": "C:\\project"}` + } else { + return `{"frontend:dir": "./frontend", "projectdir": "/home/user/project"}` + } + }(), + want: func() string { + if runtime.GOOS == "windows" { + return `C:/project/frontend` + } else { + return `/home/user/project/frontend` + } + }(), + wantError: false, + }, + { + name: "Should honour an absolute path", + inputJSON: func() string { + if runtime.GOOS == "windows" { + return `{"frontend:dir": "C:\\frontend", "projectdir": "C:\\project"}` + } else { + return `{"frontend:dir": "/home/myproject/frontend", "projectdir": "/home/user/project"}` + } + }(), + want: func() string { + if runtime.GOOS == "windows" { + return `C:/frontend` + } else { + return `/home/myproject/frontend` + } + }(), + wantError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proj, err := project.Parse([]byte(tt.inputJSON)) + if err != nil && !tt.wantError { + t.Errorf("Error parsing project: %s", err) + } + got := proj.GetFrontendDir() + got = filepath.ToSlash(got) + if got != tt.want { + t.Errorf("GetFrontendDir() = %v, want %v", got, tt.want) + } + }) + } +} +func TestProject_GetBuildDir(t *testing.T) { + cwd := lo.Must(os.Getwd()) + tests := []struct { + name string + inputJSON string + want string + wantError bool + }{ + { + name: "Should use 'build' by default", + inputJSON: "{}", + want: filepath.ToSlash(filepath.Join(cwd, "build")), + wantError: false, + }, + { + name: "Should resolve a relative path with no project path", + inputJSON: `{"build:dir": "./build"}`, + want: filepath.ToSlash(filepath.Join(cwd, "build")), + wantError: false, + }, + { + name: "Should resolve a relative path with project path set", + inputJSON: `{"build:dir": "./build", "projectdir": "/home/user/project"}`, + want: "/home/user/project/build", + wantError: false, + }, + { + name: "Should honour an absolute path", + inputJSON: func() string { + if runtime.GOOS == "windows" { + return `{"build:dir": "C:\\build", "projectdir": "C:\\project"}` + } else { + return `{"build:dir": "/home/myproject/build", "projectdir": "/home/user/project"}` + } + }(), + want: func() string { + if runtime.GOOS == "windows" { + return `C:/build` + } else { + return `/home/myproject/build` + } + }(), + wantError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proj, err := project.Parse([]byte(tt.inputJSON)) + if err != nil && !tt.wantError { + t.Errorf("Error parsing project: %s", err) + } + got := proj.GetBuildDir() + got = filepath.ToSlash(got) + if got != tt.want { + t.Errorf("GetFrontendDir() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/internal/s/s.go b/v2/internal/s/s.go new file mode 100644 index 000000000..adb304178 --- /dev/null +++ b/v2/internal/s/s.go @@ -0,0 +1,312 @@ +package s + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/bitfield/script" +) + +var ( + Output io.Writer = io.Discard + IndentSize int + originalOutput io.Writer + currentIndent int +) + +func checkError(err error) { + if err != nil { + println("\nERROR:", err.Error()) + os.Exit(1) + } +} + +func mute() { + originalOutput = Output + Output = io.Discard +} + +func unmute() { + Output = originalOutput +} + +func indent() { + currentIndent += IndentSize +} + +func unindent() { + currentIndent -= IndentSize +} + +func log(message string, args ...interface{}) { + indent := strings.Repeat(" ", currentIndent) + _, err := fmt.Fprintf(Output, indent+message+"\n", args...) + checkError(err) +} + +// RENAME a file or directory +func RENAME(source string, target string) { + log("RENAME %s -> %s", source, target) + err := os.Rename(source, target) + checkError(err) +} + +// MUSTDELETE a file. +func MUSTDELETE(filename string) { + log("DELETE %s", filename) + err := os.Remove(filepath.Join(CWD(), filename)) + checkError(err) +} + +// DELETE a file. +func DELETE(filename string) { + log("DELETE %s", filename) + _ = os.Remove(filepath.Join(CWD(), filename)) +} + +func CD(dir string) { + err := os.Chdir(dir) + checkError(err) + log("CD %s [%s]", dir, CWD()) +} + +func MKDIR(path string, mode ...os.FileMode) { + var perms os.FileMode + perms = 0o755 + if len(mode) == 1 { + perms = mode[0] + } + log("MKDIR %s (perms: %v)", path, perms) + err := os.MkdirAll(path, perms) + checkError(err) +} + +// ENDIR ensures that the path gets created if it doesn't exist +func ENDIR(path string, mode ...os.FileMode) { + var perms os.FileMode + perms = 0o755 + if len(mode) == 1 { + perms = mode[0] + } + _ = os.MkdirAll(path, perms) +} + +// COPYDIR recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 +func COPYDIR(src string, dst string) { + log("COPYDIR %s -> %s", src, dst) + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + checkError(err) + if !si.IsDir() { + checkError(fmt.Errorf("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + checkError(err) + } + if err == nil { + checkError(fmt.Errorf("destination already exists")) + } + + indent() + MKDIR(dst) + + entries, err := os.ReadDir(src) + checkError(err) + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + COPYDIR(srcPath, dstPath) + } else { + // Skip symlinks. + if entry.Type()&os.ModeSymlink != 0 { + continue + } + + COPY(srcPath, dstPath) + } + } + unindent() +} + +// COPY file from source to target +func COPY(source string, target string) { + log("COPY %s -> %s", source, target) + src, err := os.Open(source) + checkError(err) + defer closefile(src) + d, err := os.Create(target) + checkError(err) + defer closefile(d) + _, err = io.Copy(d, src) + checkError(err) +} + +func CWD() string { + result, err := os.Getwd() + checkError(err) + log("CWD [%s]", result) + return result +} + +func RMDIR(target string) { + log("RMDIR %s", target) + err := os.RemoveAll(target) + checkError(err) +} + +func RM(target string) { + log("RM %s", target) + err := os.Remove(target) + checkError(err) +} + +func ECHO(message string) { + println(message) +} + +func TOUCH(filepath string) { + log("TOUCH %s", filepath) + f, err := os.Create(filepath) + checkError(err) + closefile(f) +} + +func EXEC(command string) { + log("EXEC %s", command) + gen := script.Exec(command) + gen.Wait() + checkError(gen.Error()) +} + +// EXISTS - Returns true if the given path exists +func EXISTS(path string) bool { + _, err := os.Lstat(path) + log("EXISTS %s (%T)", path, err == nil) + return err == nil +} + +// ISDIR returns true if the given directory exists +func ISDIR(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsDir() +} + +// ISDIREMPTY returns true if the given directory is empty +func ISDIREMPTY(dir string) bool { + // CREDIT: https://stackoverflow.com/a/30708914/8325411 + f, err := os.Open(dir) + checkError(err) + defer closefile(f) + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + return err == io.EOF +} + +// ISFILE returns true if the given file exists +func ISFILE(path string) bool { + fi, err := os.Lstat(path) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// SUBDIRS returns a list of subdirectories for the given directory +func SUBDIRS(rootDir string) []string { + var result []string + + // Iterate root dir + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + checkError(err) + // If we have a directory, save it + if info.IsDir() { + result = append(result, path) + } + return nil + }) + checkError(err) + return result +} + +// SAVESTRING will create a file with the given string +func SAVESTRING(filename string, data string) { + log("SAVESTRING %s", filename) + mute() + SAVEBYTES(filename, []byte(data)) + unmute() +} + +// LOADSTRING returns the contents of the given filename as a string +func LOADSTRING(filename string) string { + log("LOADSTRING %s", filename) + mute() + data := LOADBYTES(filename) + unmute() + return string(data) +} + +// SAVEBYTES will create a file with the given string +func SAVEBYTES(filename string, data []byte) { + log("SAVEBYTES %s", filename) + err := os.WriteFile(filename, data, 0o755) + checkError(err) +} + +// LOADBYTES returns the contents of the given filename as a string +func LOADBYTES(filename string) []byte { + log("LOADBYTES %s", filename) + data, err := os.ReadFile(filename) + checkError(err) + return data +} + +func closefile(f *os.File) { + err := f.Close() + checkError(err) +} + +// MD5FILE returns the md5sum of the given file +func MD5FILE(filename string) string { + f, err := os.Open(filename) + checkError(err) + defer closefile(f) + + h := md5.New() + _, err = io.Copy(h, f) + checkError(err) + + return hex.EncodeToString(h.Sum(nil)) +} + +// Sub is the substitution type +type Sub map[string]string + +// REPLACEALL replaces all substitution keys with associated values in the given file +func REPLACEALL(filename string, substitutions Sub) { + log("REPLACEALL %s (%v)", filename, substitutions) + data := LOADSTRING(filename) + for old, newText := range substitutions { + data = strings.ReplaceAll(data, old, newText) + } + SAVESTRING(filename, data) +} diff --git a/v2/internal/shell/env.go b/v2/internal/shell/env.go new file mode 100644 index 000000000..ad6a64360 --- /dev/null +++ b/v2/internal/shell/env.go @@ -0,0 +1,40 @@ +package shell + +import ( + "fmt" + "strings" +) + +func UpsertEnv(env []string, key string, update func(v string) string) []string { + newEnv := make([]string, len(env), len(env)+1) + found := false + for i := range env { + if strings.HasPrefix(env[i], key+"=") { + eqIndex := strings.Index(env[i], "=") + val := env[i][eqIndex+1:] + newEnv[i] = fmt.Sprintf("%s=%v", key, update(val)) + found = true + continue + } + newEnv[i] = env[i] + } + if !found { + newEnv = append(newEnv, fmt.Sprintf("%s=%v", key, update(""))) + } + return newEnv +} + +func RemoveEnv(env []string, key string) []string { + newEnv := make([]string, 0, len(env)) + for _, e := range env { + if strings.HasPrefix(e, key+"=") { + continue + } + newEnv = append(newEnv, e) + } + return newEnv +} + +func SetEnv(env []string, key string, value string) []string { + return UpsertEnv(env, key, func(_ string) string { return value }) +} diff --git a/v2/internal/shell/env_test.go b/v2/internal/shell/env_test.go new file mode 100644 index 000000000..ca41c84dc --- /dev/null +++ b/v2/internal/shell/env_test.go @@ -0,0 +1,67 @@ +package shell + +import "testing" + +func TestUpdateEnv(t *testing.T) { + + env := []string{"one=1", "two=a=b", "three="} + newEnv := UpsertEnv(env, "two", func(v string) string { + return v + "+added" + }) + newEnv = UpsertEnv(newEnv, "newVar", func(v string) string { + return "added" + }) + newEnv = UpsertEnv(newEnv, "three", func(v string) string { + return "3" + }) + newEnv = UpsertEnv(newEnv, "GOARCH", func(v string) string { + return "amd64" + }) + + if len(newEnv) != 5 { + t.Errorf("expected: 5, got: %d", len(newEnv)) + } + if newEnv[1] != "two=a=b+added" { + t.Errorf("expected: \"two=a=b+added\", got: %q", newEnv[1]) + } + if newEnv[2] != "three=3" { + t.Errorf("expected: \"three=3\", got: %q", newEnv[2]) + } + if newEnv[3] != "newVar=added" { + t.Errorf("expected: \"newVar=added\", got: %q", newEnv[3]) + } + if newEnv[4] != "GOARCH=amd64" { + t.Errorf("expected: \"newVar=added\", got: %q", newEnv[4]) + } +} + +func TestSetEnv(t *testing.T) { + env := []string{"one=1", "two=a=b", "three="} + newEnv := SetEnv(env, "two", "set") + newEnv = SetEnv(newEnv, "newVar", "added") + + if len(newEnv) != 4 { + t.Errorf("expected: 4, got: %d", len(newEnv)) + } + if newEnv[1] != "two=set" { + t.Errorf("expected: \"two=set\", got: %q", newEnv[1]) + } + if newEnv[3] != "newVar=added" { + t.Errorf("expected: \"newVar=added\", got: %q", newEnv[3]) + } +} + +func TestRemoveEnv(t *testing.T) { + env := []string{"one=1", "two=a=b", "three=3"} + newEnv := RemoveEnv(env, "two") + + if len(newEnv) != 2 { + t.Errorf("expected: 2, got: %d", len(newEnv)) + } + if newEnv[0] != "one=1" { + t.Errorf("expected: \"one=1\", got: %q", newEnv[1]) + } + if newEnv[1] != "three=3" { + t.Errorf("expected: \"three=3\", got: %q", newEnv[3]) + } +} diff --git a/v2/internal/shell/shell.go b/v2/internal/shell/shell.go new file mode 100644 index 000000000..349e27bff --- /dev/null +++ b/v2/internal/shell/shell.go @@ -0,0 +1,98 @@ +package shell + +import ( + "bytes" + "os" + "os/exec" +) + +type Command struct { + command string + args []string + env []string + dir string + stdo, stde bytes.Buffer +} + +func NewCommand(command string) *Command { + return &Command{ + command: command, + env: os.Environ(), + } +} + +func (c *Command) Dir(dir string) { + c.dir = dir +} + +func (c *Command) Env(name string, value string) { + c.env = append(c.env, name+"="+value) +} + +func (c *Command) Run() error { + cmd := exec.Command(c.command, c.args...) + if c.dir != "" { + cmd.Dir = c.dir + } + cmd.Stdout = &c.stdo + cmd.Stderr = &c.stde + return cmd.Run() +} + +func (c *Command) Stdout() string { + return c.stdo.String() +} + +func (c *Command) Stderr() string { + return c.stde.String() +} + +func (c *Command) AddArgs(args []string) { + c.args = append(c.args, args...) +} + +// CreateCommand returns a *Cmd struct that when run, will run the given command + args in the given directory +func CreateCommand(directory string, command string, args ...string) *exec.Cmd { + cmd := exec.Command(command, args...) + cmd.Dir = directory + return cmd +} + +// RunCommand will run the given command + args in the given directory +// Will return stdout, stderr and error +func RunCommand(directory string, command string, args ...string) (string, string, error) { + return RunCommandWithEnv(nil, directory, command, args...) +} + +// RunCommandWithEnv will run the given command + args in the given directory and using the specified env. +// +// Env specifies the environment of the process. Each entry is of the form "key=value". +// If Env is nil, the new process uses the current process's environment. +// +// Will return stdout, stderr and error +func RunCommandWithEnv(env []string, directory string, command string, args ...string) (string, string, error) { + cmd := CreateCommand(directory, command, args...) + cmd.Env = env + + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + err := cmd.Run() + return stdo.String(), stde.String(), err +} + +// RunCommandVerbose will run the given command + args in the given directory +// Will return an error if one occurs +func RunCommandVerbose(directory string, command string, args ...string) error { + cmd := CreateCommand(directory, command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + return err +} + +// CommandExists returns true if the given command can be found on the shell +func CommandExists(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} diff --git a/v2/internal/signal/signal.go b/v2/internal/signal/signal.go new file mode 100644 index 000000000..fa797453f --- /dev/null +++ b/v2/internal/signal/signal.go @@ -0,0 +1,38 @@ +package signal + +import ( + "os" + gosignal "os/signal" + "sync" + "syscall" +) + +var signalChannel = make(chan os.Signal, 2) + +var ( + callbacks []func() + lock sync.Mutex +) + +func OnShutdown(callback func()) { + lock.Lock() + defer lock.Unlock() + callbacks = append(callbacks, callback) +} + +// Start the Signal Manager +func Start() { + // Hook into interrupts + gosignal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + // Spin off signal listener and wait for either a cancellation + // or signal + go func() { + <-signalChannel + println("") + println("Ctrl+C detected. Shutting down...") + for _, callback := range callbacks { + callback() + } + }() +} diff --git a/v2/internal/staticanalysis/staticanalysis.go b/v2/internal/staticanalysis/staticanalysis.go new file mode 100644 index 000000000..cde436633 --- /dev/null +++ b/v2/internal/staticanalysis/staticanalysis.go @@ -0,0 +1,84 @@ +package staticanalysis + +import ( + "go/ast" + "path/filepath" + "strings" + + "golang.org/x/tools/go/packages" +) + +type EmbedDetails struct { + BaseDir string + EmbedPath string + All bool +} + +func (e *EmbedDetails) GetFullPath() string { + return filepath.Join(e.BaseDir, e.EmbedPath) +} + +func GetEmbedDetails(sourcePath string) ([]*EmbedDetails, error) { + // read in project files and determine which directories are used for embedding + // return a list of directories + + absPath, err := filepath.Abs(sourcePath) + if err != nil { + return nil, err + } + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedCompiledGoFiles, + Dir: absPath, + }, "./...") + if err != nil { + return nil, err + } + var result []*EmbedDetails + for _, pkg := range pkgs { + for index, file := range pkg.Syntax { + baseDir := filepath.Dir(pkg.CompiledGoFiles[index]) + embedPaths := GetEmbedDetailsForFile(file, baseDir) + if len(embedPaths) > 0 { + result = append(result, embedPaths...) + } + } + } + return result, nil +} + +func GetEmbedDetailsForFile(file *ast.File, baseDir string) []*EmbedDetails { + var result []*EmbedDetails + for _, comment := range file.Comments { + for _, c := range comment.List { + if strings.HasPrefix(c.Text, "//go:embed") { + sl := strings.Split(c.Text, " ") + if len(sl) == 1 { + continue + } + // support for multiple paths in one comment + for _, arg := range sl[1:] { + embedPath := strings.TrimSpace(arg) + // ignores all pattern matching characters except escape sequence + if strings.Contains(embedPath, "*") || strings.Contains(embedPath, "?") || strings.Contains(embedPath, "[") { + continue + } + if strings.HasPrefix(embedPath, "all:") { + result = append(result, &EmbedDetails{ + EmbedPath: strings.TrimPrefix(embedPath, "all:"), + All: true, + BaseDir: baseDir, + }) + } else { + result = append(result, &EmbedDetails{ + EmbedPath: embedPath, + All: false, + BaseDir: baseDir, + }) + } + + } + } + } + } + return result +} diff --git a/v2/internal/staticanalysis/staticanalysis_test.go b/v2/internal/staticanalysis/staticanalysis_test.go new file mode 100644 index 000000000..77ad2fa6c --- /dev/null +++ b/v2/internal/staticanalysis/staticanalysis_test.go @@ -0,0 +1,51 @@ +package staticanalysis + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetEmbedDetails(t *testing.T) { + type args struct { + sourcePath string + } + tests := []struct { + name string + args args + want []*EmbedDetails + wantErr bool + }{ + { + name: "GetEmbedDetails", + args: args{ + sourcePath: "test/standard", + }, + want: []*EmbedDetails{ + { + EmbedPath: "frontend/dist", + All: true, + }, + { + EmbedPath: "frontend/static", + All: false, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetEmbedDetails(tt.args.sourcePath) + if (err != nil) != tt.wantErr { + t.Errorf("GetEmbedDetails() error = %v, wantErr %v", err, tt.wantErr) + return + } + require.Equal(t, len(tt.want), len(got)) + for index, g := range got { + require.Equal(t, tt.want[index].EmbedPath, g.EmbedPath) + require.Equal(t, tt.want[index].All, g.All) + } + }) + } +} diff --git a/v2/internal/staticanalysis/test/standard/.gitignore b/v2/internal/staticanalysis/test/standard/.gitignore new file mode 100644 index 000000000..d44c22f8c --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist \ No newline at end of file diff --git a/v2/internal/staticanalysis/test/standard/README.md b/v2/internal/staticanalysis/test/standard/README.md new file mode 100644 index 000000000..397b08b92 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/internal/staticanalysis/test/standard/app.go b/v2/internal/staticanalysis/test/standard/app.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/internal/staticanalysis/test/standard/go.mod b/v2/internal/staticanalysis/test/standard/go.mod new file mode 100644 index 000000000..c9fe1fb52 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/go.mod @@ -0,0 +1,35 @@ +module changeme + +go 1.18 + +require github.com/wailsapp/wails/v2 v2.3.1 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.9.1 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.0 // indirect + github.com/leaanthony/gosod v1.0.3 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.2 // indirect + github.com/samber/lo v1.27.1 // indirect + github.com/tkrajina/go-reflector v0.5.6 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.0.0 => C:\Users\leaan diff --git a/v2/internal/staticanalysis/test/standard/go.sum b/v2/internal/staticanalysis/test/standard/go.sum new file mode 100644 index 000000000..2cd0cf773 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/go.sum @@ -0,0 +1,87 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= +github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg= +github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= +github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM= +github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/v2/internal/staticanalysis/test/standard/main.go b/v2/internal/staticanalysis/test/standard/main.go new file mode 100644 index 000000000..2b6ab33b6 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist frontend/static +var assets embed.FS + +//go:embed frontend/src/*.json +var srcjson embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "staticanalysis", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/internal/staticanalysis/test/standard/wails.json b/v2/internal/staticanalysis/test/standard/wails.json new file mode 100644 index 000000000..5ab7f3600 --- /dev/null +++ b/v2/internal/staticanalysis/test/standard/wails.json @@ -0,0 +1,12 @@ +{ + "name": "staticanalysis", + "outputfilename": "staticanalysis", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/internal/system/operatingsystem/os.go b/v2/internal/system/operatingsystem/os.go new file mode 100644 index 000000000..028a97b2e --- /dev/null +++ b/v2/internal/system/operatingsystem/os.go @@ -0,0 +1,14 @@ +package operatingsystem + +// OS contains information about the operating system +type OS struct { + ID string + Name string + Version string + Branding string +} + +// Info retrieves information about the current platform +func Info() (*OS, error) { + return platformInfo() +} diff --git a/v2/internal/system/operatingsystem/os_darwin.go b/v2/internal/system/operatingsystem/os_darwin.go new file mode 100644 index 000000000..8083e1aed --- /dev/null +++ b/v2/internal/system/operatingsystem/os_darwin.go @@ -0,0 +1,49 @@ +package operatingsystem + +import ( + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +func getSysctlValue(key string) (string, error) { + stdout, _, err := shell.RunCommand(".", "sysctl", key) + if err != nil { + return "", err + } + version := strings.TrimPrefix(stdout, key+": ") + return strings.TrimSpace(version), nil +} + +func platformInfo() (*OS, error) { + // Default value + var result OS + result.ID = "Unknown" + result.Name = "MacOS" + result.Version = "Unknown" + + version, err := getSysctlValue("kern.osproductversion") + if err != nil { + return nil, err + } + result.Version = version + ID, err := getSysctlValue("kern.osversion") + if err != nil { + return nil, err + } + result.ID = ID + + // cmd := CreateCommand(directory, command, args...) + // var stdo, stde bytes.Buffer + // cmd.Stdout = &stdo + // cmd.Stderr = &stde + // err := cmd.Run() + // return stdo.String(), stde.String(), err + // } + // sysctl := shell.NewCommand("sysctl") + // kern.ostype: Darwin + // kern.osrelease: 20.1.0 + // kern.osrevision: 199506 + + return &result, nil +} diff --git a/v2/internal/system/operatingsystem/os_linux.go b/v2/internal/system/operatingsystem/os_linux.go new file mode 100644 index 000000000..49e00c02c --- /dev/null +++ b/v2/internal/system/operatingsystem/os_linux.go @@ -0,0 +1,51 @@ +//go:build linux +// +build linux + +package operatingsystem + +import ( + "fmt" + "os" + "strings" +) + +// platformInfo is the platform specific method to get system information +func platformInfo() (*OS, error) { + _, err := os.Stat("/etc/os-release") + if os.IsNotExist(err) { + return nil, fmt.Errorf("unable to read system information") + } + + osRelease, _ := os.ReadFile("/etc/os-release") + return parseOsRelease(string(osRelease)), nil +} + +func parseOsRelease(osRelease string) *OS { + + // Default value + var result OS + result.ID = "Unknown" + result.Name = "Unknown" + result.Version = "Unknown" + + // Split into lines + lines := strings.Split(osRelease, "\n") + // Iterate lines + for _, line := range lines { + // Split each line by the equals char + splitLine := strings.SplitN(line, "=", 2) + // Check we have + if len(splitLine) != 2 { + continue + } + switch splitLine[0] { + case "ID": + result.ID = strings.ToLower(strings.Trim(splitLine[1], "\"")) + case "NAME": + result.Name = strings.Trim(splitLine[1], "\"") + case "VERSION_ID": + result.Version = strings.Trim(splitLine[1], "\"") + } + } + return &result +} diff --git a/v2/internal/system/operatingsystem/os_windows.go b/v2/internal/system/operatingsystem/os_windows.go new file mode 100644 index 000000000..a9aa05a92 --- /dev/null +++ b/v2/internal/system/operatingsystem/os_windows.go @@ -0,0 +1,67 @@ +//go:build windows + +package operatingsystem + +import ( + "fmt" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +func stripNulls(str string) string { + // Split the string into substrings at each null character + substrings := strings.Split(str, "\x00") + + // Join the substrings back into a single string + strippedStr := strings.Join(substrings, "") + + return strippedStr +} + +func mustStringToUTF16Ptr(input string) *uint16 { + input = stripNulls(input) + result, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return result +} + +func getBranding() string { + var modBranding = syscall.NewLazyDLL("winbrand.dll") + var brandingFormatString = modBranding.NewProc("BrandingFormatString") + + windowsLong := mustStringToUTF16Ptr("%WINDOWS_LONG%\x00") + ret, _, _ := brandingFormatString.Call( + uintptr(unsafe.Pointer(windowsLong)), + ) + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ret))) +} + +func platformInfo() (*OS, error) { + // Default value + var result OS + result.ID = "Unknown" + result.Name = "Windows" + result.Version = "Unknown" + + // Credit: https://stackoverflow.com/a/33288328 + // Ignore errors as it isn't a showstopper + key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + + productName, _, _ := key.GetStringValue("ProductName") + currentBuild, _, _ := key.GetStringValue("CurrentBuildNumber") + displayVersion, _, _ := key.GetStringValue("DisplayVersion") + releaseId, _, _ := key.GetStringValue("ReleaseId") + + result.Name = productName + result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild) + result.ID = displayVersion + result.Branding = getBranding() + + return &result, key.Close() +} diff --git a/v2/internal/system/operatingsystem/version_windows.go b/v2/internal/system/operatingsystem/version_windows.go new file mode 100644 index 000000000..a8f53d134 --- /dev/null +++ b/v2/internal/system/operatingsystem/version_windows.go @@ -0,0 +1,62 @@ +//go:build windows + +package operatingsystem + +import ( + "strconv" + + "golang.org/x/sys/windows/registry" +) + +type WindowsVersionInfo struct { + Major int + Minor int + Build int + DisplayVersion string +} + +func (w *WindowsVersionInfo) IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return w.Major >= major && w.Minor >= minor && w.Build >= buildNumber +} + +func GetWindowsVersionInfo() (*WindowsVersionInfo, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return nil, err + } + + return &WindowsVersionInfo{ + Major: regDWORDKeyAsInt(key, "CurrentMajorVersionNumber"), + Minor: regDWORDKeyAsInt(key, "CurrentMinorVersionNumber"), + Build: regStringKeyAsInt(key, "CurrentBuildNumber"), + DisplayVersion: regKeyAsString(key, "DisplayVersion"), + }, nil +} + +func regDWORDKeyAsInt(key registry.Key, name string) int { + result, _, err := key.GetIntegerValue(name) + if err != nil { + return -1 + } + return int(result) +} + +func regStringKeyAsInt(key registry.Key, name string) int { + resultStr, _, err := key.GetStringValue(name) + if err != nil { + return -1 + } + result, err := strconv.Atoi(resultStr) + if err != nil { + return -1 + } + return result +} + +func regKeyAsString(key registry.Key, name string) string { + resultStr, _, err := key.GetStringValue(name) + if err != nil { + return "" + } + return resultStr +} diff --git a/v2/internal/system/packagemanager/apt.go b/v2/internal/system/packagemanager/apt.go new file mode 100644 index 000000000..806d08f2d --- /dev/null +++ b/v2/internal/system/packagemanager/apt.go @@ -0,0 +1,109 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "bytes" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Apt represents the Apt manager +type Apt struct { + name string + osid string +} + +// NewApt creates a new Apt instance +func NewApt(osid string) *Apt { + return &Apt{ + name: "apt", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (a *Apt) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-dev", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "build-essential", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker.io", SystemPackage: true, Optional: true}, + }, + "nsis": []*Package{ + {Name: "nsis", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (a *Apt) Name() string { + return a.name +} + +// PackageInstalled tests if the given package name is installed +func (a *Apt) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + cmd := exec.Command("apt", "list", "-qq", pkg.Name) + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + cmd.Env = append(os.Environ(), "LANGUAGE=en") + err := cmd.Run() + return strings.Contains(stdo.String(), "[installed]"), err +} + +// PackageAvailable tests if the given package is available for installation +func (a *Apt) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "apt", "list", "-qq", pkg.Name) + // We add a space to ensure we get a full match, not partial match + output := a.removeEscapeSequences(stdout) + installed := strings.HasPrefix(output, pkg.Name) + a.getPackageVersion(pkg, output) + return installed, err +} + +// InstallCommand returns the package manager specific command to install a package +func (a *Apt) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[a.osid] + } + return "sudo apt install " + pkg.Name +} + +func (a *Apt) removeEscapeSequences(in string) string { + escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`) + return escapechars.ReplaceAllString(in, "") +} + +func (a *Apt) getPackageVersion(pkg *Package, output string) { + + splitOutput := strings.Split(output, " ") + if len(splitOutput) > 1 { + pkg.Version = splitOutput[1] + } +} diff --git a/v2/internal/system/packagemanager/dnf.go b/v2/internal/system/packagemanager/dnf.go new file mode 100644 index 000000000..fec676f11 --- /dev/null +++ b/v2/internal/system/packagemanager/dnf.go @@ -0,0 +1,133 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "os/exec" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Dnf represents the Dnf manager +type Dnf struct { + name string + osid string +} + +// NewDnf creates a new Dnf instance +func NewDnf(osid string) *Dnf { + return &Dnf{ + name: "dnf", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (y *Dnf) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk4.0-devel", SystemPackage: true, Library: true}, + {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true}, + // {Name: "webkitgtk3-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc-c++", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkgconf-pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + {Name: "nodejs-npm", SystemPackage: true}, + }, + "upx": []*Package{ + {Name: "upx", SystemPackage: true, Optional: true}, + }, + "docker": []*Package{ + { + SystemPackage: false, + Optional: true, + InstallCommand: map[string]string{ + "centos": "Follow the guide: https://docs.docker.com/engine/install/centos/", + "fedora": "Follow the guide: https://docs.docker.com/engine/install/fedora/", + }, + }, + {Name: "moby-engine", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (y *Dnf) Name() string { + return y.name +} + +// PackageInstalled tests if the given package name is installed +func (y *Dnf) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "dnf", "info", "installed", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, "Version") { + splitline := strings.Split(line, ":") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + + return true, err +} + +// PackageAvailable tests if the given package is available for installation +func (y *Dnf) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "dnf", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, "Version") { + splitline := strings.Split(line, ":") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + return true, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (y *Dnf) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[y.osid] + } + return "sudo dnf install " + pkg.Name +} + +func (y *Dnf) getPackageVersion(pkg *Package, output string) { + splitOutput := strings.Split(output, " ") + if len(splitOutput) > 0 { + pkg.Version = splitOutput[1] + } +} diff --git a/v2/internal/system/packagemanager/emerge.go b/v2/internal/system/packagemanager/emerge.go new file mode 100644 index 000000000..7497d580a --- /dev/null +++ b/v2/internal/system/packagemanager/emerge.go @@ -0,0 +1,118 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Emerge represents the Emerge package manager +type Emerge struct { + name string + osid string +} + +// NewEmerge creates a new Emerge instance +func NewEmerge(osid string) *Emerge { + return &Emerge{ + name: "emerge", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (e *Emerge) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "x11-libs/gtk+", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "net-libs/webkit-gtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "sys-devel/gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "dev-util/pkgconf", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "net-libs/nodejs", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "app-emulation/docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (e *Emerge) Name() string { + return e.name +} + +// PackageInstalled tests if the given package name is installed +func (e *Emerge) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$") + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + regex := `.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version installed: (.*)` + installedRegex := regexp.MustCompile(regex) + matches := installedRegex.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + installed := false + if noOfMatches > 1 && matches[1] != "[ Not Installed ]" { + installed = true + pkg.Version = strings.TrimSpace(matches[1]) + } + return installed, err +} + +// PackageAvailable tests if the given package is available for installation +func (e *Emerge) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$") + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + installedRegex := regexp.MustCompile(`.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version available: (.*)`) + matches := installedRegex.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + available := false + if noOfMatches > 1 { + available = true + pkg.Version = strings.TrimSpace(matches[1]) + } + return available, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (e *Emerge) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[e.osid] + } + return "sudo emerge " + pkg.Name +} diff --git a/v2/internal/system/packagemanager/eopkg.go b/v2/internal/system/packagemanager/eopkg.go new file mode 100644 index 000000000..936127eac --- /dev/null +++ b/v2/internal/system/packagemanager/eopkg.go @@ -0,0 +1,115 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "regexp" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Eopkg represents the Eopkg manager +type Eopkg struct { + name string + osid string +} + +// NewEopkg creates a new Eopkg instance +func NewEopkg(osid string) *Eopkg { + result := &Eopkg{ + name: "eopkg", + osid: osid, + } + result.intialiseName() + return result +} + +// Packages returns the packages that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (e *Eopkg) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit-gtk-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkgconf", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "nodejs", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (e *Eopkg) Name() string { + return e.name +} + +// PackageInstalled tests if the given package is installed +func (e *Eopkg) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "eopkg", "info", pkg.Name) + return strings.HasPrefix(stdout, "Installed"), err +} + +// PackageAvailable tests if the given package is available for installation +func (e *Eopkg) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "eopkg", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + output := e.removeEscapeSequences(stdout) + installed := strings.Contains(output, "Package found in Solus repository") + e.getPackageVersion(pkg, output) + return installed, err +} + +// InstallCommand returns the package manager specific command to install a package +func (e *Eopkg) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[e.osid] + } + return "sudo eopkg it " + pkg.Name +} + +func (e *Eopkg) removeEscapeSequences(in string) string { + escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`) + return escapechars.ReplaceAllString(in, "") +} + +func (e *Eopkg) intialiseName() { + result := "eopkg" + stdout, _, err := shell.RunCommand(".", "eopkg", "--version") + if err == nil { + result = strings.TrimSpace(stdout) + } + e.name = result +} + +func (e *Eopkg) getPackageVersion(pkg *Package, output string) { + + versionRegex := regexp.MustCompile(`.*Name.*version:\s+(.*)+, release: (.*)`) + matches := versionRegex.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = matches[1] + if noOfMatches > 2 { + pkg.Version += " (r" + matches[2] + ")" + } + } +} diff --git a/v2/internal/system/packagemanager/nixpkgs.go b/v2/internal/system/packagemanager/nixpkgs.go new file mode 100644 index 000000000..360473d24 --- /dev/null +++ b/v2/internal/system/packagemanager/nixpkgs.go @@ -0,0 +1,159 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "encoding/json" + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Nixpkgs represents the Nixpkgs manager +type Nixpkgs struct { + name string + osid string +} + +type NixPackageDetail struct { + Name string + Pname string + Version string +} + +var available map[string]NixPackageDetail + +// NewNixpkgs creates a new Nixpkgs instance +func NewNixpkgs(osid string) *Nixpkgs { + available = map[string]NixPackageDetail{} + + return &Nixpkgs{ + name: "nixpkgs", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (n *Nixpkgs) Packages() packagemap { + // Currently, only support checking the default channel. + channel := "nixpkgs" + if n.osid == "nixos" { + channel = "nixos" + } + + return packagemap{ + "libgtk-3": []*Package{ + {Name: channel + ".gtk3", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: channel + ".webkitgtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: channel + ".gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: channel + ".pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: channel + ".nodejs", SystemPackage: true}, + }, + "upx": []*Package{ + {Name: channel + ".upx", SystemPackage: true, Optional: true}, + }, + "docker": []*Package{ + {Name: channel + ".docker", SystemPackage: true, Optional: true}, + }, + "nsis": []*Package{ + {Name: channel + ".nsis", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (n *Nixpkgs) Name() string { + return n.name +} + +// PackageInstalled tests if the given package name is installed +func (n *Nixpkgs) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + + stdout, _, err := shell.RunCommand(".", "nix-env", "--json", "-qA", pkg.Name) + if err != nil { + return false, nil + } + + var attributes map[string]NixPackageDetail + err = json.Unmarshal([]byte(stdout), &attributes) + if err != nil { + return false, err + } + + // Did we get one? + installed := false + for attribute, detail := range attributes { + if attribute == pkg.Name { + installed = true + pkg.Version = detail.Version + } + break + } + + // If on NixOS, package may be installed via system config, so check the nix store. + detail, ok := available[pkg.Name] + if !installed && n.osid == "nixos" && ok { + cmd := "nix-store --query --requisites /run/current-system | cut -d- -f2- | sort | uniq | grep '^" + detail.Pname + "'" + + if pkg.Library { + cmd += " | grep 'dev$'" + } + + stdout, _, err = shell.RunCommand(".", "sh", "-c", cmd) + if err != nil { + return false, nil + } + + if len(stdout) > 0 { + installed = true + } + } + + return installed, nil +} + +// PackageAvailable tests if the given package is available for installation +func (n *Nixpkgs) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + + stdout, _, err := shell.RunCommand(".", "nix-env", "--json", "-qaA", pkg.Name) + if err != nil { + return false, nil + } + + var attributes map[string]NixPackageDetail + err = json.Unmarshal([]byte(stdout), &attributes) + if err != nil { + return false, err + } + + // Grab first version. + for attribute, detail := range attributes { + pkg.Version = detail.Version + available[attribute] = detail + break + } + + return len(pkg.Version) > 0, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (n *Nixpkgs) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[n.osid] + } + return "nix-env -iA " + pkg.Name +} diff --git a/v2/internal/system/packagemanager/packagemanager.go b/v2/internal/system/packagemanager/packagemanager.go new file mode 100644 index 000000000..043c8e3cf --- /dev/null +++ b/v2/internal/system/packagemanager/packagemanager.go @@ -0,0 +1,162 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "sort" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// A list of package manager commands +var pmcommands = []string{ + "eopkg", + "apt", + "dnf", + "pacman", + "emerge", + "zypper", + "nix-env", +} + +// Find will attempt to find the system package manager +func Find(osid string) PackageManager { + + // Loop over pmcommands + for _, pmname := range pmcommands { + if shell.CommandExists(pmname) { + return newPackageManager(pmname, osid) + } + } + return nil +} + +func newPackageManager(pmname string, osid string) PackageManager { + switch pmname { + case "eopkg": + return NewEopkg(osid) + case "apt": + return NewApt(osid) + case "dnf": + return NewDnf(osid) + case "pacman": + return NewPacman(osid) + case "emerge": + return NewEmerge(osid) + case "zypper": + return NewZypper(osid) + case "nix-env": + return NewNixpkgs(osid) + } + return nil +} + +// Dependencies scans the system for required dependencies +// Returns a list of dependencies search for, whether they were found +// and whether they were installed +func Dependencies(p PackageManager) (DependencyList, error) { + + var dependencies DependencyList + + for name, packages := range p.Packages() { + dependency := &Dependency{Name: name} + for _, pkg := range packages { + dependency.Optional = pkg.Optional + dependency.External = !pkg.SystemPackage + dependency.InstallCommand = p.InstallCommand(pkg) + packageavailable, err := p.PackageAvailable(pkg) + if err != nil { + return nil, err + } + if packageavailable { + dependency.Version = pkg.Version + dependency.PackageName = pkg.Name + installed, err := p.PackageInstalled(pkg) + if err != nil { + return nil, err + } + if installed { + dependency.Installed = true + dependency.Version = pkg.Version + if !pkg.SystemPackage { + dependency.Version = AppVersion(name) + } + } else { + dependency.InstallCommand = p.InstallCommand(pkg) + } + break + } + } + dependencies = append(dependencies, dependency) + } + + // Sort dependencies + sort.Slice(dependencies, func(i, j int) bool { + return dependencies[i].Name < dependencies[j].Name + }) + + return dependencies, nil +} + +// AppVersion returns the version for application related to the given package +func AppVersion(name string) string { + + if name == "gcc" { + return gccVersion() + } + + if name == "pkg-config" { + return pkgConfigVersion() + } + + if name == "npm" { + return npmVersion() + } + + if name == "docker" { + return dockerVersion() + } + + return "" + +} + +func gccVersion() string { + + var version string + var err error + + // Try "-dumpfullversion" + version, _, err = shell.RunCommand(".", "gcc", "-dumpfullversion") + if err != nil { + + // Try -dumpversion + // We ignore the error as this function is not for testing whether the + // application exists, only that we can get the version number + dumpversion, _, err := shell.RunCommand(".", "gcc", "-dumpversion") + if err == nil { + version = dumpversion + } + } + return strings.TrimSpace(version) +} + +func pkgConfigVersion() string { + version, _, _ := shell.RunCommand(".", "pkg-config", "--version") + return strings.TrimSpace(version) +} + +func npmVersion() string { + version, _, _ := shell.RunCommand(".", "npm", "--version") + return strings.TrimSpace(version) +} + +func dockerVersion() string { + version, _, _ := shell.RunCommand(".", "docker", "--version") + version = strings.TrimPrefix(version, "Docker version ") + version = strings.ReplaceAll(version, ", build ", " (") + version = strings.TrimSpace(version) + ")" + return version +} diff --git a/v2/internal/system/packagemanager/pacman.go b/v2/internal/system/packagemanager/pacman.go new file mode 100644 index 000000000..1fbecf781 --- /dev/null +++ b/v2/internal/system/packagemanager/pacman.go @@ -0,0 +1,115 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Pacman represents the Pacman package manager +type Pacman struct { + name string + osid string +} + +// NewPacman creates a new Pacman instance +func NewPacman(osid string) *Pacman { + return &Pacman{ + name: "pacman", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (p *Pacman) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkgconf", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (p *Pacman) Name() string { + return p.name +} + +// PackageInstalled tests if the given package name is installed +func (p *Pacman) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := shell.RunCommand(".", "pacman", "-Q", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, pkg.Name) { + splitline := strings.Split(line, " ") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + + return true, err +} + +// PackageAvailable tests if the given package is available for installation +func (p *Pacman) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + output, _, err := shell.RunCommand(".", "pacman", "-Si", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + reg := regexp.MustCompile(`.*Version.*?:\s+(.*)`) + matches := reg.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = strings.TrimSpace(matches[1]) + } + + return true, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (p *Pacman) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[p.osid] + } + return "sudo pacman -S " + pkg.Name +} diff --git a/v2/internal/system/packagemanager/pm.go b/v2/internal/system/packagemanager/pm.go new file mode 100644 index 000000000..bba45cd05 --- /dev/null +++ b/v2/internal/system/packagemanager/pm.go @@ -0,0 +1,60 @@ +package packagemanager + +// Package contains information about a system package +type Package struct { + Name string + Version string + InstallCommand map[string]string + SystemPackage bool + Library bool + Optional bool +} + +type packagemap = map[string][]*Package + +// PackageManager is a common interface across all package managers +type PackageManager interface { + Name() string + Packages() packagemap + PackageInstalled(pkg *Package) (bool, error) + PackageAvailable(pkg *Package) (bool, error) + InstallCommand(pkg *Package) string +} + +// Dependency represents a system package that we require +type Dependency struct { + Name string + PackageName string + Installed bool + InstallCommand string + Version string + Optional bool + External bool +} + +// DependencyList is a list of Dependency instances +type DependencyList []*Dependency + +// InstallAllRequiredCommand returns the command you need to use to install all required dependencies +func (d DependencyList) InstallAllRequiredCommand() string { + result := "" + for _, dependency := range d { + if !dependency.Installed && !dependency.Optional { + result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n" + } + } + + return result +} + +// InstallAllOptionalCommand returns the command you need to use to install all optional dependencies +func (d DependencyList) InstallAllOptionalCommand() string { + result := "" + for _, dependency := range d { + if !dependency.Installed && dependency.Optional { + result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n" + } + } + + return result +} diff --git a/v2/internal/system/packagemanager/zypper.go b/v2/internal/system/packagemanager/zypper.go new file mode 100644 index 000000000..efaeb0b1b --- /dev/null +++ b/v2/internal/system/packagemanager/zypper.go @@ -0,0 +1,128 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +// Zypper represents the Zypper package manager +type Zypper struct { + name string + osid string +} + +// NewZypper creates a new Zypper instance +func NewZypper(osid string) *Zypper { + return &Zypper{ + name: "zypper", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (z *Zypper) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk3-soup2-devel", SystemPackage: true, Library: true}, + {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc-c++", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + {Name: "pkgconf-pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm10", SystemPackage: true}, + {Name: "npm20", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (z *Zypper) Name() string { + return z.name +} + +// PackageInstalled tests if the given package name is installed +func (z *Zypper) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + var env []string + env = shell.SetEnv(env, "LANGUAGE", "en_US.utf-8") + stdout, _, err := shell.RunCommandWithEnv(env, ".", "zypper", "info", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + reg := regexp.MustCompile(`.*Installed\s*:\s*(Yes)\s*`) + matches := reg.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + z.getPackageVersion(pkg, stdout) + } + return noOfMatches > 1, err +} + +// PackageAvailable tests if the given package is available for installation +func (z *Zypper) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + var env []string + env = shell.SetEnv(env, "LANGUAGE", "en_US.utf-8") + stdout, _, err := shell.RunCommandWithEnv(env, ".", "zypper", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + available := strings.Contains(stdout, "Information for package") + if available { + z.getPackageVersion(pkg, stdout) + } + + return available, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (z *Zypper) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[z.osid] + } + return "sudo zypper in " + pkg.Name +} + +func (z *Zypper) getPackageVersion(pkg *Package, output string) { + + reg := regexp.MustCompile(`.*Version.*:(.*)`) + matches := reg.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = strings.TrimSpace(matches[1]) + } +} diff --git a/v2/internal/system/system.go b/v2/internal/system/system.go new file mode 100644 index 000000000..67453538f --- /dev/null +++ b/v2/internal/system/system.go @@ -0,0 +1,169 @@ +package system + +import ( + "os/exec" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" +) + +var IsAppleSilicon bool + +// Info holds information about the current operating system, +// package manager and required dependencies +type Info struct { + OS *operatingsystem.OS + PM packagemanager.PackageManager + Dependencies packagemanager.DependencyList +} + +// GetInfo scans the system for operating system details, +// the system package manager and the status of required +// dependencies. +func GetInfo() (*Info, error) { + var result Info + err := result.discover() + if err != nil { + return nil, err + } + return &result, nil +} + +func checkNodejs() *packagemanager.Dependency { + // Check for Nodejs + output, err := exec.Command("node", "-v").Output() + installed := true + version := "" + if err != nil { + installed = false + } else { + if len(output) > 0 { + version = strings.TrimSpace(strings.Split(string(output), "\n")[0])[1:] + } + } + return &packagemanager.Dependency{ + Name: "Nodejs", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://nodejs.org/en/download/", + Version: version, + Optional: false, + External: false, + } +} + +func checkNPM() *packagemanager.Dependency { + // Check for npm + output, err := exec.Command("npm", "-version").Output() + installed := true + version := "" + if err != nil { + installed = false + } else { + version = strings.TrimSpace(strings.Split(string(output), "\n")[0]) + } + return &packagemanager.Dependency{ + Name: "npm ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://nodejs.org/en/download/", + Version: version, + Optional: false, + External: false, + } +} + +func checkUPX() *packagemanager.Dependency { + // Check for npm + output, err := exec.Command("upx", "-V").Output() + installed := true + version := "" + if err != nil { + installed = false + } else { + version = strings.TrimSpace(strings.Split(string(output), "\n")[0]) + } + return &packagemanager.Dependency{ + Name: "upx ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://upx.github.io/", + Version: version, + Optional: true, + External: false, + } +} + +func checkNSIS() *packagemanager.Dependency { + // Check for nsis installer + output, err := exec.Command("makensis", "-VERSION").Output() + installed := true + version := "" + if err != nil { + installed = false + } else { + version = strings.TrimSpace(strings.Split(string(output), "\n")[0]) + } + return &packagemanager.Dependency{ + Name: "nsis ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "More info at https://wails.io/docs/guides/windows-installer/", + Version: version, + Optional: true, + External: false, + } +} + +func checkLibrary(name string) func() *packagemanager.Dependency { + return func() *packagemanager.Dependency { + output, _, _ := shell.RunCommand(".", "pkg-config", "--cflags", name) + installed := len(strings.TrimSpace(output)) > 0 + + return &packagemanager.Dependency{ + Name: "lib" + name + " ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Install via your package manager", + Version: "N/A", + Optional: false, + External: false, + } + } +} + +func checkDocker() *packagemanager.Dependency { + // Check for npm + output, err := exec.Command("docker", "version").Output() + installed := true + version := "" + + // Docker errors if it is not running so check for that + if len(output) == 0 && err != nil { + installed = false + } else { + // Version is in a line like: " Version: 20.10.5" + versionOutput := strings.Split(string(output), "\n") + for _, line := range versionOutput[1:] { + splitLine := strings.Split(line, ":") + if len(splitLine) > 1 { + key := strings.TrimSpace(splitLine[0]) + if key == "Version" { + version = strings.TrimSpace(splitLine[1]) + break + } + } + } + } + return &packagemanager.Dependency{ + Name: "docker ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://www.docker.com/products/docker-desktop", + Version: version, + Optional: true, + External: false, + } +} diff --git a/v2/internal/system/system_darwin.go b/v2/internal/system/system_darwin.go new file mode 100644 index 000000000..16dfd8873 --- /dev/null +++ b/v2/internal/system/system_darwin.go @@ -0,0 +1,91 @@ +//go:build darwin +// +build darwin + +package system + +import ( + "fmt" + "os/exec" + "strings" + "syscall" + + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" +) + +// Determine if the app is running on Apple Silicon +// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/ +func init() { + r, err := syscall.Sysctl("sysctl.proc_translated") + if err != nil { + return + } + + IsAppleSilicon = r == "\x00\x00\x00" || r == "\x01\x00\x00" +} + +func (i *Info) discover() error { + var err error + osinfo, err := operatingsystem.Info() + if err != nil { + return err + } + i.OS = osinfo + + i.Dependencies = append(i.Dependencies, checkXCodeSelect()) + i.Dependencies = append(i.Dependencies, checkNodejs()) + i.Dependencies = append(i.Dependencies, checkNPM()) + i.Dependencies = append(i.Dependencies, checkXCodeBuild()) + i.Dependencies = append(i.Dependencies, checkUPX()) + i.Dependencies = append(i.Dependencies, checkNSIS()) + return nil +} + +func checkXCodeSelect() *packagemanager.Dependency { + // Check for xcode command line tools + output, err := exec.Command("xcode-select", "-v").Output() + installed := true + version := "" + if err != nil { + installed = false + } else { + version = strings.TrimPrefix(string(output), "xcode-select version ") + version = strings.TrimSpace(version) + version = strings.TrimSuffix(version, ".") + } + return &packagemanager.Dependency{ + Name: "Xcode command line tools ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "xcode-select --install", + Version: version, + Optional: false, + External: false, + } +} + +func checkXCodeBuild() *packagemanager.Dependency { + // Check for xcode + output, err := exec.Command("xcodebuild", "-version").Output() + installed := true + version := "" + if err != nil { + installed = false + } else if l := strings.Split(string(output), "\n"); len(l) >= 2 { + version = fmt.Sprintf("%s (%s)", + strings.TrimPrefix(l[0], "Xcode "), + strings.TrimPrefix(l[1], "Build version ")) + } else { + version = "N/A" + } + + return &packagemanager.Dependency{ + Name: "Xcode", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://apps.apple.com/us/app/xcode/id497799835", + Version: version, + Optional: true, + External: false, + } +} diff --git a/v2/internal/system/system_linux.go b/v2/internal/system/system_linux.go new file mode 100644 index 000000000..703e978eb --- /dev/null +++ b/v2/internal/system/system_linux.go @@ -0,0 +1,94 @@ +//go:build linux +// +build linux + +package system + +import ( + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" +) + +func checkGCC() *packagemanager.Dependency { + + version := packagemanager.AppVersion("gcc") + + return &packagemanager.Dependency{ + Name: "gcc ", + PackageName: "N/A", + Installed: version != "", + InstallCommand: "Install via your package manager", + Version: version, + Optional: false, + External: false, + } +} + +func checkPkgConfig() *packagemanager.Dependency { + + version := packagemanager.AppVersion("pkg-config") + + return &packagemanager.Dependency{ + Name: "pkg-config ", + PackageName: "N/A", + Installed: version != "", + InstallCommand: "Install via your package manager", + Version: version, + Optional: false, + External: false, + } +} + +func checkLocallyInstalled(checker func() *packagemanager.Dependency, dependency *packagemanager.Dependency) { + if !dependency.Installed { + locallyInstalled := checker() + if locallyInstalled.Installed { + dependency.Installed = true + dependency.Version = locallyInstalled.Version + } + } +} + +var checkerFunctions = map[string]func() *packagemanager.Dependency{ + "Nodejs": checkNodejs, + "npm": checkNPM, + "docker": checkDocker, + "upx": checkUPX, + "gcc": checkGCC, + "pkg-config": checkPkgConfig, + "libgtk-3": checkLibrary("libgtk-3"), + "libwebkit": checkLibrary("libwebkit"), +} + +func (i *Info) discover() error { + + var err error + osinfo, err := operatingsystem.Info() + if err != nil { + return err + } + i.OS = osinfo + + i.PM = packagemanager.Find(osinfo.ID) + if i.PM != nil { + dependencies, err := packagemanager.Dependencies(i.PM) + if err != nil { + return err + } + for _, dep := range dependencies { + checker := checkerFunctions[dep.Name] + if checker != nil { + checkLocallyInstalled(checker, dep) + } + if dep.Name == "nsis" { + locallyInstalled := checkNSIS() + if locallyInstalled.Installed { + dep.Installed = true + dep.Version = locallyInstalled.Version + } + } + } + i.Dependencies = dependencies + } + + return nil +} diff --git a/v2/internal/system/system_windows.go b/v2/internal/system/system_windows.go new file mode 100644 index 000000000..40b8f0340 --- /dev/null +++ b/v2/internal/system/system_windows.go @@ -0,0 +1,45 @@ +//go:build windows +// +build windows + +package system + +import ( + "github.com/wailsapp/go-webview2/webviewloader" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" +) + +func (i *Info) discover() error { + + var err error + osinfo, err := operatingsystem.Info() + if err != nil { + return err + } + i.OS = osinfo + + i.Dependencies = append(i.Dependencies, checkWebView2()) + i.Dependencies = append(i.Dependencies, checkNodejs()) + i.Dependencies = append(i.Dependencies, checkNPM()) + i.Dependencies = append(i.Dependencies, checkUPX()) + i.Dependencies = append(i.Dependencies, checkNSIS()) + // i.Dependencies = append(i.Dependencies, checkDocker()) + + return nil +} + +func checkWebView2() *packagemanager.Dependency { + version, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString("") + installed := version != "" + + return &packagemanager.Dependency{ + Name: "WebView2 ", + PackageName: "N/A", + Installed: installed, + InstallCommand: "Available at https://developer.microsoft.com/en-us/microsoft-edge/webview2/", + Version: version, + Optional: false, + External: true, + } + +} diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt b/v2/internal/typescriptify/LICENSE.txt similarity index 97% rename from cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt rename to v2/internal/typescriptify/LICENSE.txt index 75b52484e..fa6e64ac4 100644 --- a/cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt +++ b/v2/internal/typescriptify/LICENSE.txt @@ -1,202 +1,202 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2015-] [Tomo Krajina] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/v2/internal/typescriptify/README.md b/v2/internal/typescriptify/README.md new file mode 100644 index 000000000..b5c961835 --- /dev/null +++ b/v2/internal/typescriptify/README.md @@ -0,0 +1,2 @@ +Based on: https://github.com/tkrajina/typescriptify-golang-structs +License: LICENSE.txt \ No newline at end of file diff --git a/v2/internal/typescriptify/js-reserved-keywords.go b/v2/internal/typescriptify/js-reserved-keywords.go new file mode 100644 index 000000000..4f9aa09f4 --- /dev/null +++ b/v2/internal/typescriptify/js-reserved-keywords.go @@ -0,0 +1,69 @@ +package typescriptify + +var jsReservedKeywords []string = []string{ + "abstract", + "arguments", + "await", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "double", + "else", + "enum", + "eval", + "export", + "extends", + "false", + "final", + "finally", + "float", + "for", + "function", + "goto", + "if", + "implements", + "import", + "in", + "instanceof", + "int", + "interface", + "let", + "long", + "native", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "typeof", + "var", + "void", + "volatile", + "while", + "with", + "yield", + "object", +} diff --git a/v2/internal/typescriptify/typescriptify.go b/v2/internal/typescriptify/typescriptify.go new file mode 100644 index 000000000..e732c5976 --- /dev/null +++ b/v2/internal/typescriptify/typescriptify.go @@ -0,0 +1,1011 @@ +package typescriptify + +import ( + "bufio" + "cmp" + "fmt" + "io" + "log" + "os" + "path" + "reflect" + "regexp" + "slices" + "strings" + "time" + + "github.com/leaanthony/slicer" + + "github.com/tkrajina/go-reflector/reflector" +) + +const ( + tsTransformTag = "ts_transform" + tsType = "ts_type" + tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; +}` + jsVariableNameRegex = `^([A-Z]|[a-z]|\$|_)([A-Z]|[a-z]|[0-9]|\$|_)*$` +) + +var jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`) + +func nameTypeOf(typeOf reflect.Type) string { + tname := typeOf.Name() + gidx := strings.IndexRune(tname, '[') + if gidx > 0 { // its a generic type + rem := strings.SplitN(tname, "[", 2) + tname = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_") + } + return tname +} + +// TypeOptions overrides options set by `ts_*` tags. +type TypeOptions struct { + TSType string + TSTransform string +} + +// StructType stores settings for transforming one Golang struct. +type StructType struct { + Type reflect.Type + FieldOptions map[reflect.Type]TypeOptions +} + +func NewStruct(i interface{}) *StructType { + return &StructType{ + Type: reflect.TypeOf(i), + } +} + +func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType { + if st.FieldOptions == nil { + st.FieldOptions = map[reflect.Type]TypeOptions{} + } + var typ reflect.Type + if ty, is := i.(reflect.Type); is { + typ = ty + } else { + typ = reflect.TypeOf(i) + } + st.FieldOptions[typ] = opts + return st +} + +type EnumType struct { + Type reflect.Type +} + +type enumElement struct { + value interface{} + name string +} + +type TypeScriptify struct { + Prefix string + Suffix string + Indent string + CreateFromMethod bool + CreateConstructor bool + BackupDir string // If empty no backup + DontExport bool + CreateInterface bool + customImports []string + + structTypes []StructType + enumTypes []EnumType + enums map[reflect.Type][]enumElement + kinds map[reflect.Kind]string + + fieldTypeOptions map[reflect.Type]TypeOptions + + // throwaway, used when converting + alreadyConverted map[string]bool + + Namespace string + KnownStructs *slicer.StringSlicer + KnownEnums *slicer.StringSlicer +} + +func New() *TypeScriptify { + result := new(TypeScriptify) + result.Indent = "\t" + result.BackupDir = "." + + kinds := make(map[reflect.Kind]string) + + kinds[reflect.Bool] = "boolean" + kinds[reflect.Interface] = "any" + + kinds[reflect.Int] = "number" + kinds[reflect.Int8] = "number" + kinds[reflect.Int16] = "number" + kinds[reflect.Int32] = "number" + kinds[reflect.Int64] = "number" + kinds[reflect.Uint] = "number" + kinds[reflect.Uint8] = "number" + kinds[reflect.Uint16] = "number" + kinds[reflect.Uint32] = "number" + kinds[reflect.Uint64] = "number" + kinds[reflect.Float32] = "number" + kinds[reflect.Float64] = "number" + + kinds[reflect.String] = "string" + + result.kinds = kinds + + result.Indent = " " + result.CreateFromMethod = true + result.CreateConstructor = true + + return result +} + +func (t *TypeScriptify) deepFields(typeOf reflect.Type) []reflect.StructField { + fields := make([]reflect.StructField, 0) + + if typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + + if typeOf.Kind() != reflect.Struct { + return fields + } + + for i := 0; i < typeOf.NumField(); i++ { + f := typeOf.Field(i) + kind := f.Type.Kind() + isPointer := kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct + if f.Anonymous && kind == reflect.Struct { + // fmt.Println(v.Interface()) + fields = append(fields, t.deepFields(f.Type)...) + } else if f.Anonymous && isPointer { + // fmt.Println(v.Interface()) + fields = append(fields, t.deepFields(f.Type.Elem())...) + } else { + // Check we have a json tag + jsonTag := t.getJSONFieldName(f, isPointer) + if jsonTag != "" { + fields = append(fields, f) + } + } + } + + return fields +} + +func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) { + fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...) +} + +// ManageType can define custom options for fields of a specified type. +// +// This can be used instead of setting ts_type and ts_transform for all fields of a certain type. +func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify { + var typ reflect.Type + switch t := fld.(type) { + case reflect.Type: + typ = t + default: + typ = reflect.TypeOf(fld) + } + if t.fieldTypeOptions == nil { + t.fieldTypeOptions = map[reflect.Type]TypeOptions{} + } + t.fieldTypeOptions[typ] = opts + return t +} + +func (t *TypeScriptify) GetGeneratedStructs() []string { + var result []string + for key := range t.alreadyConverted { + result = append(result, key) + } + return result +} + +func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify { + t.CreateFromMethod = b + return t +} + +func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify { + t.CreateInterface = b + return t +} + +func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify { + t.CreateConstructor = b + return t +} + +func (t *TypeScriptify) WithIndent(i string) *TypeScriptify { + t.Indent = i + return t +} + +func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify { + t.BackupDir = b + return t +} + +func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify { + t.Prefix = p + return t +} + +func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify { + t.Suffix = s + return t +} + +func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify { + switch ty := obj.(type) { + case StructType: + t.structTypes = append(t.structTypes, ty) + case *StructType: + t.structTypes = append(t.structTypes, *ty) + case reflect.Type: + t.AddType(ty) + default: + t.AddType(reflect.TypeOf(obj)) + } + return t +} + +func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { + t.structTypes = append(t.structTypes, StructType{Type: typeOf}) + return t +} + +func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { + keyType := field.Type.Key() + valueType := field.Type.Elem() + valueTypeName := nameTypeOf(valueType) + valueTypeSuffix := "" + valueTypePrefix := "" + if valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + } + if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { + arrayDepth := 1 + for valueType.Elem().Kind() == reflect.Array || valueType.Elem().Kind() == reflect.Slice { + valueType = valueType.Elem() + arrayDepth++ + } + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + valueTypeSuffix = strings.Repeat(">", arrayDepth) + valueTypePrefix = strings.Repeat("Array<", arrayDepth) + } + if valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + } + if name, ok := t.types[valueType.Kind()]; ok { + valueTypeName = name + } + if valueType.Kind() == reflect.Map { + // TODO: support nested maps + valueTypeName = "any" // valueType.Elem().Name() + } + if valueType.Kind() == reflect.Struct && differentNamespaces(t.namespace, valueType) { + valueTypeName = valueType.String() + } + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + isOptional := strings.HasSuffix(fieldName, "?") + + keyTypeStr := "" + // Key should always be a JS primitive. JS will read it as a string either way. + if typeStr, isSimple := t.types[keyType.Kind()]; isSimple { + keyTypeStr = typeStr + } else { + keyTypeStr = t.types[reflect.String] + } + + var dotField string + if regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) { + dotField = fmt.Sprintf(".%s", strippedFieldName) + } else { + dotField = fmt.Sprintf(`["%s"]`, strippedFieldName) + if isOptional { + fieldName = fmt.Sprintf(`"%s"?`, strippedFieldName) + } + } + t.fields = append(t.fields, fmt.Sprintf("%s%s: Record<%s, %s>;", t.indent, fieldName, keyTypeStr, valueTypePrefix+valueTypeName+valueTypeSuffix)) + if valueType.Kind() == reflect.Struct { + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);", + t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypePrefix+valueTypeName+valueTypeSuffix+t.suffix)) + } else { + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = source[\"%s\"];", + t.indent, t.indent, dotField, strippedFieldName)) + } +} + +func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { + if t.enums == nil { + t.enums = map[reflect.Type][]enumElement{} + } + items := reflect.ValueOf(values) + if items.Kind() != reflect.Slice { + panic(fmt.Sprintf("Values for %T isn't a slice", values)) + } + + var elements []enumElement + for i := 0; i < items.Len(); i++ { + item := items.Index(i) + + var el enumElement + if item.Kind() == reflect.Struct { + r := reflector.New(item.Interface()) + val, err := r.Field("Value").Get() + if err != nil { + panic(fmt.Sprint("missing Type field in ", item.Type().String())) + } + name, err := r.Field("TSName").Get() + if err != nil { + panic(fmt.Sprint("missing TSName field in ", item.Type().String())) + } + el.value = val + el.name = name.(string) + } else { + el.value = item.Interface() + if tsNamer, is := item.Interface().(TSNamer); is { + el.name = tsNamer.TSName() + } else { + panic(fmt.Sprint(item.Type().String(), " has no TSName method")) + } + } + + elements = append(elements, el) + } + slices.SortFunc(elements, func(a, b enumElement) int { + return cmp.Compare(a.name, b.name) + }) + ty := reflect.TypeOf(elements[0].value) + t.enums[ty] = elements + t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) + + return t +} + +// AddEnumValues is deprecated, use `AddEnum()` +func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify { + t.AddEnum(values) + return t +} + +func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) { + t.alreadyConverted = make(map[string]bool) + depth := 0 + + result := "" + if len(t.customImports) > 0 { + // Put the custom imports, i.e.: `import Decimal from 'decimal.js'` + for _, cimport := range t.customImports { + result += cimport + "\n" + } + } + + for _, enumTyp := range t.enumTypes { + elements := t.enums[enumTyp.Type] + typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + + for _, strctTyp := range t.structTypes { + typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode) + if err != nil { + return "", err + } + result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n") + } + return result, nil +} + +func loadCustomCode(fileName string) (map[string]string, error) { + result := make(map[string]string) + f, err := os.Open(fileName) + if err != nil { + if os.IsNotExist(err) { + return result, nil + } + return result, err + } + defer f.Close() + + bytes, err := io.ReadAll(f) + if err != nil { + return result, err + } + + var currentName string + var currentValue string + lines := strings.Split(string(bytes), "\n") + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") { + currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1) + currentValue = "" + } else if trimmedLine == "//[end]" { + result[currentName] = strings.TrimRight(currentValue, " \t\r\n") + currentName = "" + currentValue = "" + } else if len(currentName) > 0 { + currentValue += line + "\n" + } + } + + return result, nil +} + +func (t TypeScriptify) backup(fileName string) error { + fileIn, err := os.Open(fileName) + if err != nil { + if !os.IsNotExist(err) { + return err + } + // No neet to backup, just return: + return nil + } + defer fileIn.Close() + + bytes, err := io.ReadAll(fileIn) + if err != nil { + return err + } + + _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99"))) + if t.BackupDir != "" { + backupFn = path.Join(t.BackupDir, backupFn) + } + + return os.WriteFile(backupFn, bytes, os.FileMode(0o700)) +} + +func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error { + if len(t.BackupDir) > 0 { + err := t.backup(fileName) + if err != nil { + return err + } + } + + customCode, err := loadCustomCode(fileName) + if err != nil { + return err + } + + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + + converted, err := t.Convert(customCode) + if err != nil { + return err + } + + var lines []string + sc := bufio.NewScanner(strings.NewReader(converted)) + for sc.Scan() { + lines = append(lines, "\t"+sc.Text()) + } + + converted = "export namespace " + packageName + " {\n" + converted += strings.Join(lines, "\n") + converted += "\n}\n" + + if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil { + return err + } + if _, err := f.WriteString(converted); err != nil { + return err + } + + return nil +} + +type TSNamer interface { + TSName() string +} + +func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) { + t.logf(depth, "Converting enum %s", typeOf.String()) + if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted + return "", nil + } + t.alreadyConverted[typeOf.String()] = true + + entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix + result := "enum " + entityName + " {\n" + + for _, val := range elements { + result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value) + } + + result += "}" + + if !t.DontExport { + result = "export " + result + } + + return result, nil +} + +func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions { + // By default use options defined by tags: + opts := TypeOptions{TSTransform: field.Tag.Get(tsTransformTag), TSType: field.Tag.Get(tsType)} + + overrides := []TypeOptions{} + + // But there is maybe an struct-specific override: + for _, strct := range t.structTypes { + if strct.FieldOptions == nil { + continue + } + if strct.Type == structType { + if fldOpts, found := strct.FieldOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + } + } + + if fldOpts, found := t.fieldTypeOptions[field.Type]; found { + overrides = append(overrides, fldOpts) + } + + for _, o := range overrides { + if o.TSTransform != "" { + opts.TSTransform = o.TSTransform + } + if o.TSType != "" { + opts.TSType = o.TSType + } + } + + return opts +} + +func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { + jsonFieldName := "" + // function, complex, and channel types cannot be json-encoded + if field.Type.Kind() == reflect.Chan || + field.Type.Kind() == reflect.Func || + field.Type.Kind() == reflect.UnsafePointer || + field.Type.Kind() == reflect.Complex128 || + field.Type.Kind() == reflect.Complex64 { + return "" + } + jsonTag, hasTag := field.Tag.Lookup("json") + if !hasTag && field.IsExported() { + jsonFieldName = field.Name + if isPtr { + jsonFieldName += "?" + } + } + if len(jsonTag) > 0 { + jsonTagParts := strings.Split(jsonTag, ",") + if len(jsonTagParts) > 0 { + jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent) + } + hasOmitEmpty := false + ignored := false + for _, t := range jsonTagParts { + if t == "" { + break + } + if t == "omitempty" { + hasOmitEmpty = true + break + } + if t == "-" { + ignored = true + break + } + } + if !ignored && isPtr || hasOmitEmpty { + jsonFieldName = fmt.Sprintf("%s?", jsonFieldName) + } + } + return jsonFieldName +} + +func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) { + if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted + return "", nil + } + fields := t.deepFields(typeOf) + t.logf(depth, "Converting type %s", typeOf.String()) + if differentNamespaces(t.Namespace, typeOf) { + return "", nil + } + + t.alreadyConverted[typeOf.String()] = true + + entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix + + if typeClashWithReservedKeyword(entityName) { + warnAboutTypesClash(entityName) + } + + result := "" + if t.CreateInterface { + result += fmt.Sprintf("interface %s {\n", entityName) + } else { + result += fmt.Sprintf("class %s {\n", entityName) + } + if !t.DontExport { + result = "export " + result + } + builder := typeScriptClassBuilder{ + types: t.kinds, + indent: t.Indent, + prefix: t.Prefix, + suffix: t.Suffix, + namespace: t.Namespace, + } + + for _, field := range fields { + isPtr := field.Type.Kind() == reflect.Ptr + if isPtr { + field.Type = field.Type.Elem() + } + jsonFieldName := t.getJSONFieldName(field, isPtr) + if len(jsonFieldName) == 0 || jsonFieldName == "-" { + continue + } + + var err error + fldOpts := t.getFieldOptions(typeOf, field) + if fldOpts.TSTransform != "" { + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if _, isEnum := t.enums[field.Type]; isEnum { + t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name) + builder.AddEnumField(jsonFieldName, field) + } else if fldOpts.TSType != "" { // Struct: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } else if field.Type.Kind() == reflect.Struct { // Struct: + t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + + // Anonymous structures is ignored + // It is possible to generate them but hard to generate correct name + if field.Type.Name() != "" { + typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + + isKnownType := t.KnownStructs.Contains(getStructFQN(field.Type.String())) + if !isKnownType { + println("KnownStructs:", t.KnownStructs.Join("\t")) + println("Not found:", getStructFQN(field.Type.String())) + } + builder.AddStructField(jsonFieldName, field, !isKnownType) + } else if field.Type.Kind() == reflect.Map { + t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) + // Also convert map key types if needed + var keyTypeToConvert reflect.Type + switch field.Type.Key().Kind() { + case reflect.Struct: + keyTypeToConvert = field.Type.Key() + case reflect.Ptr: + keyTypeToConvert = field.Type.Key().Elem() + } + if keyTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + // Also convert map value types if needed + var valueTypeToConvert reflect.Type + switch field.Type.Elem().Kind() { + case reflect.Struct: + valueTypeToConvert = field.Type.Elem() + case reflect.Ptr: + valueTypeToConvert = field.Type.Elem().Elem() + } + if valueTypeToConvert != nil { + typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + } + + builder.AddMapField(jsonFieldName, field) + } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: + if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type + field.Type = field.Type.Elem() + } + + arrayDepth := 1 + for field.Type.Elem().Kind() == reflect.Slice || field.Type.Elem().Kind() == reflect.Array { // Slice of slices: + field.Type = field.Type.Elem() + arrayDepth++ + } + + if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type + field.Type = field.Type.Elem() + } + + if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: + t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) + typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) + if err != nil { + return "", err + } + if typeScriptChunk != "" { + result = typeScriptChunk + "\n" + result + } + builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth) + } else { // Slice of simple fields: + t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name) + err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts) + } + } else { // Simple field: + t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) + // check if type is in known enum. If so, then replace TStype with enum name to avoid missing types + isKnownEnum := t.KnownEnums.Contains(getStructFQN(field.Type.String())) + if isKnownEnum { + err = builder.AddSimpleField(jsonFieldName, field, TypeOptions{ + TSType: getStructFQN(field.Type.String()), + TSTransform: fldOpts.TSTransform, + }) + } else { + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } + } + if err != nil { + return "", err + } + } + + if t.CreateFromMethod { + t.CreateConstructor = true + } + + result += strings.Join(builder.fields, "\n") + "\n" + if !t.CreateInterface { + constructorBody := strings.Join(builder.constructorBody, "\n") + needsConvertValue := strings.Contains(constructorBody, "this.convertValues") + if t.CreateFromMethod { + result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent) + result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName) + result += fmt.Sprintf("%s}\n", t.Indent) + } + if t.CreateConstructor { + result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent) + result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n" + result += constructorBody + "\n" + result += fmt.Sprintf("%s}\n", t.Indent) + } + if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) { + result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n" + } + } + + if customCode != nil { + code := customCode[entityName] + if len(code) != 0 { + result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n" + } + } + + result += "}" + + return result, nil +} + +func (t *TypeScriptify) AddImport(i string) { + for _, cimport := range t.customImports { + if cimport == i { + return + } + } + + t.customImports = append(t.customImports, i) +} + +type typeScriptClassBuilder struct { + types map[reflect.Kind]string + indent string + fields []string + createFromMethodBody []string + constructorBody []string + prefix, suffix string + namespace string +} + +func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { + fieldType := nameTypeOf(field.Type.Elem()) + kind := field.Type.Elem().Kind() + typeScriptType, ok := t.types[kind] + if !ok { + typeScriptType = "any" + } + + if len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + if len(opts.TSType) > 0 { + t.addField(fieldName, opts.TSType, false) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } else if len(typeScriptType) > 0 { + t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)), false) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + return nil + } + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { + fieldType := nameTypeOf(field.Type) + kind := field.Type.Kind() + + typeScriptType, ok := t.types[kind] + if !ok { + typeScriptType = "any" + } + + if len(opts.TSType) > 0 { + typeScriptType = opts.TSType + } + + if len(typeScriptType) > 0 && len(fieldName) > 0 { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, typeScriptType, false) + if opts.TSTransform == "" { + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) + } else { + val := fmt.Sprintf(`source["%s"]`, strippedFieldName) + expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1) + t.addInitializerFieldLine(strippedFieldName, expression) + } + return nil + } + + return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType) +} + +func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { + fieldType := nameTypeOf(field.Type) + t.addField(fieldName, t.prefix+fieldType+t.suffix, false) + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) +} + +func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField, isAnyType bool) { + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + classname := "null" + namespace := strings.Split(field.Type.String(), ".")[0] + fqname := t.prefix + nameTypeOf(field.Type) + t.suffix + if namespace != t.namespace { + fqname = namespace + "." + fqname + } + + if !isAnyType { + classname = fqname + } + + // Anonymous struct + if field.Type.Name() == "" { + classname = "Object" + } + + t.addField(fieldName, fqname, isAnyType) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, classname)) +} + +func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { + fieldType := nameTypeOf(field.Type.Elem()) + if differentNamespaces(t.namespace, field.Type.Elem()) { + fieldType = field.Type.Elem().String() + } + strippedFieldName := strings.ReplaceAll(fieldName, "?", "") + t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)), false) + t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix)) +} + +func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) { + var dotField string + if regexp.MustCompile(jsVariableNameRegex).Match([]byte(fld)) { + dotField = fmt.Sprintf(".%s", fld) + } else { + dotField = fmt.Sprintf(`["%s"]`, fld) + } + t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result", dotField, " = ", initializer, ";")) + t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this", dotField, " = ", initializer, ";")) +} + +func (t *typeScriptClassBuilder) addField(fld, fldType string, isAnyType bool) { + isOptional := strings.HasSuffix(fld, "?") + strippedFieldName := strings.ReplaceAll(fld, "?", "") + if !regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) { + fld = fmt.Sprintf(`"%s"`, strippedFieldName) + if isOptional { + fld += "?" + } + } + if isAnyType { + fldType = strings.Split(fldType, ".")[0] + t.fields = append(t.fields, fmt.Sprint(t.indent, "// Go type: ", fldType, "\n", t.indent, fld, ": any;")) + } else { + t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";")) + } +} + +func indentLines(str string, i int) string { + lines := strings.Split(str, "\n") + for n := range lines { + lines[n] = strings.Repeat("\t", i) + lines[n] + } + return strings.Join(lines, "\n") +} + +func getStructFQN(in string) string { + result := strings.ReplaceAll(in, "[]", "") + result = strings.ReplaceAll(result, "*", "") + return result +} + +func differentNamespaces(namespace string, typeOf reflect.Type) bool { + if strings.ContainsRune(typeOf.String(), '.') { + typeNamespace := strings.Split(typeOf.String(), ".")[0] + if namespace != typeNamespace { + return true + } + } + return false +} + +func typeClashWithReservedKeyword(input string) bool { + in := strings.ToLower(strings.TrimSpace(input)) + for _, v := range jsReservedKeywords { + if in == v { + return true + } + } + + return false +} + +func warnAboutTypesClash(entity string) { + // TODO: Refactor logging + l := log.New(os.Stderr, "", 0) + l.Printf("Usage of reserved keyword found and not supported: %s", entity) + log.Println("Please rename returned type or consider adding bindings config to your wails.json") +} diff --git a/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe b/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe new file mode 100644 index 000000000..89a56ec16 Binary files /dev/null and b/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe differ diff --git a/v2/internal/webview2runtime/webview2installer.go b/v2/internal/webview2runtime/webview2installer.go new file mode 100644 index 000000000..3645dae02 --- /dev/null +++ b/v2/internal/webview2runtime/webview2installer.go @@ -0,0 +1,21 @@ +package webview2runtime + +import ( + _ "embed" + "os" + "path/filepath" +) + +//go:embed MicrosoftEdgeWebview2Setup.exe +var setupexe []byte + +// WriteInstallerToFile writes the installer file to the given file. +func WriteInstallerToFile(targetFile string) error { + return os.WriteFile(targetFile, setupexe, 0o755) +} + +// WriteInstaller writes the installer exe file to the given directory and returns the path to it. +func WriteInstaller(targetPath string) (string, error) { + installer := filepath.Join(targetPath, `MicrosoftEdgeWebview2Setup.exe`) + return installer, WriteInstallerToFile(installer) +} diff --git a/v2/internal/webview2runtime/webview2runtime.go b/v2/internal/webview2runtime/webview2runtime.go new file mode 100644 index 000000000..c5f6c0d53 --- /dev/null +++ b/v2/internal/webview2runtime/webview2runtime.go @@ -0,0 +1,169 @@ +//go:build windows +// +build windows + +package webview2runtime + +import ( + _ "embed" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "syscall" + "unsafe" +) + +// Info contains all the information about an installation of the webview2 runtime. +type Info struct { + Location string + Name string + Version string + SilentUninstall string +} + +// IsOlderThan returns true if the installed version is older than the given required version. +// Returns error if something goes wrong. +func (i *Info) IsOlderThan(requiredVersion string) (bool, error) { + var mod = syscall.NewLazyDLL("WebView2Loader.dll") + var CompareBrowserVersions = mod.NewProc("CompareBrowserVersions") + v1, err := syscall.UTF16PtrFromString(i.Version) + if err != nil { + return false, err + } + v2, err := syscall.UTF16PtrFromString(requiredVersion) + if err != nil { + return false, err + } + var result int = 9 + _, _, err = CompareBrowserVersions.Call(uintptr(unsafe.Pointer(v1)), uintptr(unsafe.Pointer(v2)), uintptr(unsafe.Pointer(&result))) + if result < -1 || result > 1 { + return false, err + } + return result == -1, nil +} + +func downloadBootstrapper() (string, error) { + bootstrapperURL := `https://go.microsoft.com/fwlink/p/?LinkId=2124703` + installer := filepath.Join(os.TempDir(), `MicrosoftEdgeWebview2Setup.exe`) + + // Download installer + out, err := os.Create(installer) + defer out.Close() + if err != nil { + return "", err + } + resp, err := http.Get(bootstrapperURL) + defer resp.Body.Close() + if err != nil { + err = out.Close() + return "", err + } + _, err = io.Copy(out, resp.Body) + if err != nil { + return "", err + } + + return installer, nil +} + +// InstallUsingEmbeddedBootstrapper will download the bootstrapper from Microsoft and run it to install +// the latest version of the runtime. +// Returns true if the installer ran successfully. +// Returns an error if something goes wrong +func InstallUsingEmbeddedBootstrapper() (bool, error) { + installer, err := WriteInstaller(os.TempDir()) + if err != nil { + return false, err + } + result, err := runInstaller(installer) + if err != nil { + return false, err + } + + return result, os.Remove(installer) + +} + +// InstallUsingBootstrapper will extract the embedded bootstrapper from Microsoft and run it to install +// the latest version of the runtime. +// Returns true if the installer ran successfully. +// Returns an error if something goes wrong +func InstallUsingBootstrapper() (bool, error) { + + installer, err := downloadBootstrapper() + if err != nil { + return false, err + } + + result, err := runInstaller(installer) + if err != nil { + return false, err + } + + return result, os.Remove(installer) + +} + +func runInstaller(installer string) (bool, error) { + // Credit: https://stackoverflow.com/a/10385867 + cmd := exec.Command(installer) + if err := cmd.Start(); err != nil { + return false, err + } + if err := cmd.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return status.ExitStatus() == 0, nil + } + } + } + return true, nil +} + +// Confirm will prompt the user with a message and OK / CANCEL buttons. +// Returns true if OK is selected by the user. +// Returns an error if something went wrong. +func Confirm(caption string, title string) (bool, error) { + var flags uint = 0x00000001 // MB_OKCANCEL + result, err := MessageBox(caption, title, flags) + if err != nil { + return false, err + } + return result == 1, nil +} + +// Error will an error message to the user. +// Returns an error if something went wrong. +func Error(caption string, title string) error { + var flags uint = 0x00000010 // MB_ICONERROR + _, err := MessageBox(caption, title, flags) + return err +} + +// MessageBox prompts the user with the given caption and title. +// Flags may be provided to customise the dialog. +// Returns an error if something went wrong. +func MessageBox(caption string, title string, flags uint) (int, error) { + captionUTF16, err := syscall.UTF16PtrFromString(caption) + if err != nil { + return -1, err + } + titleUTF16, err := syscall.UTF16PtrFromString(title) + if err != nil { + return -1, err + } + ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call( + uintptr(0), + uintptr(unsafe.Pointer(captionUTF16)), + uintptr(unsafe.Pointer(titleUTF16)), + uintptr(flags)) + + return int(ret), nil +} + +// OpenInstallerDownloadWebpage will open the browser on the WebView2 download page +func OpenInstallerDownloadWebpage() error { + cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://developer.microsoft.com/en-us/microsoft-edge/webview2/") + return cmd.Run() +} diff --git a/v2/internal/wv2installer/browser.go b/v2/internal/wv2installer/browser.go new file mode 100644 index 000000000..2597bde6b --- /dev/null +++ b/v2/internal/wv2installer/browser.go @@ -0,0 +1,25 @@ +//go:build windows && wv2runtime.browser +// +build windows,wv2runtime.browser + +package wv2installer + +import ( + "fmt" + "github.com/wailsapp/wails/v2/internal/webview2runtime" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error { + confirmed, err := webview2runtime.Confirm(messages.DownloadPage+MinimumRuntimeVersion, messages.MissingRequirements) + if err != nil { + return err + } + if confirmed { + err = webview2runtime.OpenInstallerDownloadWebpage() + if err != nil { + return err + } + } + + return fmt.Errorf(messages.FailedToInstall) +} diff --git a/v2/internal/wv2installer/download.go b/v2/internal/wv2installer/download.go new file mode 100644 index 000000000..0a054d661 --- /dev/null +++ b/v2/internal/wv2installer/download.go @@ -0,0 +1,35 @@ +//go:build windows && !wv2runtime.error && !wv2runtime.browser && !wv2runtime.embed +// +build windows,!wv2runtime.error,!wv2runtime.browser,!wv2runtime.embed + +package wv2installer + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/internal/webview2runtime" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error { + message := messages.InstallationRequired + if installStatus == needsUpdating { + message = messages.UpdateRequired + } + confirmed, err := webview2runtime.Confirm(message, messages.MissingRequirements) + if err != nil { + return err + } + if !confirmed { + return fmt.Errorf(messages.Webview2NotInstalled) + } + installedCorrectly, err := webview2runtime.InstallUsingBootstrapper() + if err != nil { + _ = webview2runtime.Error(err.Error(), messages.Error) + return err + } + if !installedCorrectly { + err = webview2runtime.Error(messages.FailedToInstall, messages.Error) + return err + } + return nil +} diff --git a/v2/internal/wv2installer/embed.go b/v2/internal/wv2installer/embed.go new file mode 100644 index 000000000..942d6b51a --- /dev/null +++ b/v2/internal/wv2installer/embed.go @@ -0,0 +1,35 @@ +//go:build windows && wv2runtime.embed +// +build windows,wv2runtime.embed + +package wv2installer + +import ( + "fmt" + "github.com/wailsapp/wails/v2/internal/webview2runtime" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error { + message := messages.InstallationRequired + if installStatus == needsUpdating { + message = messages.UpdateRequired + } + message += messages.PressOKToInstall + confirmed, err := webview2runtime.Confirm(message, messages.MissingRequirements) + if err != nil { + return err + } + if !confirmed { + return fmt.Errorf(messages.Webview2NotInstalled) + } + installedCorrectly, err := webview2runtime.InstallUsingEmbeddedBootstrapper() + if err != nil { + _ = webview2runtime.Error(err.Error(), messages.Error) + return err + } + if !installedCorrectly { + err = webview2runtime.Error(messages.FailedToInstall, messages.Error) + return err + } + return nil +} diff --git a/v2/internal/wv2installer/error.go b/v2/internal/wv2installer/error.go new file mode 100644 index 000000000..ec48ef990 --- /dev/null +++ b/v2/internal/wv2installer/error.go @@ -0,0 +1,15 @@ +//go:build windows && wv2runtime.error +// +build windows,wv2runtime.error + +package wv2installer + +import ( + "fmt" + "github.com/wailsapp/wails/v2/internal/webview2runtime" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error { + _ = webview2runtime.Error(messages.ContactAdmin, messages.Error) + return fmt.Errorf(messages.Webview2NotInstalled) +} diff --git a/v2/internal/wv2installer/wv2installer.go b/v2/internal/wv2installer/wv2installer.go new file mode 100644 index 000000000..c89ad196f --- /dev/null +++ b/v2/internal/wv2installer/wv2installer.go @@ -0,0 +1,60 @@ +//go:build windows + +package wv2installer + +import ( + "fmt" + + "github.com/wailsapp/go-webview2/webviewloader" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +const MinimumRuntimeVersion string = "94.0.992.31" // WebView2 SDK 1.0.992.28 + +type installationStatus int + +const ( + needsInstalling installationStatus = iota + needsUpdating +) + +func Process(appoptions *options.App) (string, error) { + messages := windows.DefaultMessages() + if appoptions.Windows != nil && appoptions.Windows.Messages != nil { + messages = appoptions.Windows.Messages + } + + installStatus := needsInstalling + + // Override version check for manually specified webview path if present + var webviewPath = "" + if opts := appoptions.Windows; opts != nil && opts.WebviewBrowserPath != "" { + webviewPath = opts.WebviewBrowserPath + } + + installedVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(webviewPath) + if err != nil { + return "", err + } + + if installedVersion != "" { + installStatus = needsUpdating + compareResult, err := webviewloader.CompareBrowserVersions(installedVersion, MinimumRuntimeVersion) + if err != nil { + return "", err + } + updateRequired := compareResult < 0 + // Installed and does not require updating + if !updateRequired { + return installedVersion, nil + } + } + + // Force error strategy if webview is manually specified + if webviewPath != "" { + return installedVersion, fmt.Errorf(messages.InvalidFixedWebview2) + } + + return installedVersion, doInstallationStrategy(installStatus, messages) +} diff --git a/v2/pkg/application/application.go b/v2/pkg/application/application.go new file mode 100644 index 000000000..8ba586969 --- /dev/null +++ b/v2/pkg/application/application.go @@ -0,0 +1,102 @@ +package application + +import ( + "context" + "sync" + + "github.com/wailsapp/wails/v2/internal/app" + "github.com/wailsapp/wails/v2/internal/signal" + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" +) + +// Application is the main Wails application +type Application struct { + application *app.App + options *options.App + + // running flag + running bool + + shutdown sync.Once +} + +// NewWithOptions creates a new Application with the given options +func NewWithOptions(options *options.App) *Application { + if options == nil { + return New() + } + return &Application{ + options: options, + } +} + +// New creates a new Application with the default options +func New() *Application { + return &Application{ + options: &options.App{}, + } +} + +// SetApplicationMenu sets the application menu +func (a *Application) SetApplicationMenu(appMenu *menu.Menu) { + if a.running { + a.application.SetApplicationMenu(appMenu) + return + } + + a.options.Menu = appMenu +} + +// Run starts the application +func (a *Application) Run() error { + err := applicationInit() + if err != nil { + return err + } + + application, err := app.CreateApp(a.options) + if err != nil { + return err + } + + a.application = application + + // Control-C handlers + signal.OnShutdown(func() { + a.application.Shutdown() + }) + signal.Start() + + a.running = true + + err = a.application.Run() + return err +} + +// Quit will shut down the application +func (a *Application) Quit() { + a.shutdown.Do(func() { + a.application.Shutdown() + }) +} + +// Bind the given struct to the application +func (a *Application) Bind(boundStruct any) { + a.options.Bind = append(a.options.Bind, boundStruct) +} + +func (a *Application) On(eventType EventType, callback func()) { + c := func(ctx context.Context) { + callback() + } + + switch eventType { + case StartUp: + a.options.OnStartup = c + case ShutDown: + a.options.OnShutdown = c + case DomReady: + a.options.OnDomReady = c + } +} diff --git a/v2/pkg/application/events.go b/v2/pkg/application/events.go new file mode 100644 index 000000000..3896e9e75 --- /dev/null +++ b/v2/pkg/application/events.go @@ -0,0 +1,9 @@ +package application + +type EventType int + +const ( + StartUp EventType = iota + ShutDown + DomReady +) diff --git a/v2/pkg/application/init.go b/v2/pkg/application/init.go new file mode 100644 index 000000000..0fc48cb05 --- /dev/null +++ b/v2/pkg/application/init.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package application + +func applicationInit() error { + return nil +} diff --git a/v2/pkg/application/init_windows.go b/v2/pkg/application/init_windows.go new file mode 100644 index 000000000..7d2900d3d --- /dev/null +++ b/v2/pkg/application/init_windows.go @@ -0,0 +1,16 @@ +//go:build windows + +package application + +import ( + "fmt" + "syscall" +) + +func applicationInit() error { + status, r, err := syscall.NewLazyDLL("user32.dll").NewProc("SetProcessDPIAware").Call() + if status == 0 { + return fmt.Errorf("exit status %d: %v %v", status, r, err) + } + return nil +} diff --git a/v2/pkg/assetserver/assethandler.go b/v2/pkg/assetserver/assethandler.go new file mode 100644 index 000000000..b8e2df076 --- /dev/null +++ b/v2/pkg/assetserver/assethandler.go @@ -0,0 +1,205 @@ +package assetserver + +import ( + "bytes" + "embed" + "errors" + "fmt" + "io" + iofs "io/fs" + "net/http" + "os" + "path" + "strconv" + "strings" + + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +type Logger interface { + Debug(message string, args ...interface{}) + Error(message string, args ...interface{}) +} + +//go:embed defaultindex.html +var defaultHTML []byte + +const ( + indexHTML = "index.html" +) + +type assetHandler struct { + fs iofs.FS + handler http.Handler + + logger Logger + + retryMissingFiles bool +} + +func NewAssetHandler(options assetserver.Options, log Logger) (http.Handler, error) { + vfs := options.Assets + if vfs != nil { + if _, err := vfs.Open("."); err != nil { + return nil, err + } + + subDir, err := FindPathToFile(vfs, indexHTML) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + msg := "no `index.html` could be found in your Assets fs.FS" + if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs { + rootFolder, _ := FindEmbedRootPath(embedFs) + msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder) + } + + return nil, fmt.Errorf(msg) + } + + return nil, err + } + + vfs, err = iofs.Sub(vfs, path.Clean(subDir)) + if err != nil { + return nil, err + } + } + + var result http.Handler = &assetHandler{ + fs: vfs, + handler: options.Handler, + logger: log, + } + + if middleware := options.Middleware; middleware != nil { + result = middleware(result) + } + + return result, nil +} + +func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + url := req.URL.Path + handler := d.handler + if strings.EqualFold(req.Method, http.MethodGet) { + filename := path.Clean(strings.TrimPrefix(url, "/")) + + d.logDebug("Handling request '%s' (file='%s')", url, filename) + if err := d.serveFSFile(rw, req, filename); err != nil { + if os.IsNotExist(err) { + if handler != nil { + d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url) + handler.ServeHTTP(rw, req) + err = nil + } else { + rw.WriteHeader(http.StatusNotFound) + err = nil + } + } + + if err != nil { + d.logError("Unable to handle request '%s': %s", url, err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + } + } + } else if handler != nil { + d.logDebug("No GET request, serving '%s' by AssetHandler", url) + handler.ServeHTTP(rw, req) + } else { + rw.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// serveFSFile will try to load the file from the fs.FS and write it to the response +func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error { + if d.fs == nil { + return os.ErrNotExist + } + + file, err := d.fs.Open(filename) + if err != nil { + return err + } + defer file.Close() + + statInfo, err := file.Stat() + if err != nil { + return err + } + + url := req.URL.Path + isDirectoryPath := url == "" || url[len(url)-1] == '/' + if statInfo.IsDir() { + if !isDirectoryPath { + // If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on + // WebKit WebViews (macOS/Linux). + // So we handle this as a specific error + return fmt.Errorf("a directory has been requested without a trailing slash, please add a trailing slash to your request") + } + + filename = path.Join(filename, indexHTML) + + file, err = d.fs.Open(filename) + if err != nil { + return err + } + defer file.Close() + + statInfo, err = file.Stat() + if err != nil { + return err + } + } else if isDirectoryPath { + return fmt.Errorf("a file has been requested with a trailing slash, please remove the trailing slash from your request") + } + + var buf [512]byte + var n int + if _, haveType := rw.Header()[HeaderContentType]; !haveType { + // Detect MimeType by sniffing the first 512 bytes + n, err = file.Read(buf[:]) + if err != nil && err != io.EOF { + return err + } + + // Do the custom MimeType sniffing even though http.ServeContent would do it in case + // of an io.ReadSeeker. We would like to have a consistent behaviour in both cases. + if contentType := GetMimetype(filename, buf[:n]); contentType != "" { + rw.Header().Set(HeaderContentType, contentType) + } + } + + if fileSeeker, _ := file.(io.ReadSeeker); fileSeeker != nil { + if _, err := fileSeeker.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("seeker can't seek") + } + + http.ServeContent(rw, req, statInfo.Name(), statInfo.ModTime(), fileSeeker) + return nil + } + + size := strconv.FormatInt(statInfo.Size(), 10) + rw.Header().Set(HeaderContentLength, size) + + // Write the first 512 bytes used for MimeType sniffing + _, err = io.Copy(rw, bytes.NewReader(buf[:n])) + if err != nil { + return err + } + + // Copy the remaining content of the file + _, err = io.Copy(rw, file) + return err +} + +func (d *assetHandler) logDebug(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Debug("[AssetHandler] "+message, args...) + } +} + +func (d *assetHandler) logError(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Error("[AssetHandler] "+message, args...) + } +} diff --git a/v2/pkg/assetserver/assethandler_external.go b/v2/pkg/assetserver/assethandler_external.go new file mode 100644 index 000000000..98b3404e9 --- /dev/null +++ b/v2/pkg/assetserver/assethandler_external.go @@ -0,0 +1,84 @@ +package assetserver + +import ( + "errors" + "fmt" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "net/http" + "net/http/httputil" + "net/url" +) + +func NewProxyServer(proxyURL string) http.Handler { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + panic(err) + } + return httputil.NewSingleHostReverseProxy(parsedURL) +} + +func NewExternalAssetsHandler(logger Logger, options assetserver.Options, url *url.URL) http.Handler { + baseHandler := options.Handler + + errSkipProxy := fmt.Errorf("skip proxying") + + proxy := httputil.NewSingleHostReverseProxy(url) + baseDirector := proxy.Director + proxy.Director = func(r *http.Request) { + baseDirector(r) + if logger != nil { + logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL) + } + } + + proxy.ModifyResponse = func(res *http.Response) error { + if baseHandler == nil { + return nil + } + + if res.StatusCode == http.StatusSwitchingProtocols { + return nil + } + + if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed { + return errSkipProxy + } + + return nil + } + + proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) { + if baseHandler != nil && errors.Is(err, errSkipProxy) { + if logger != nil { + logger.Debug("[ExternalAssetHandler] '%s' returned not found, using AssetHandler", r.URL) + } + baseHandler.ServeHTTP(rw, r) + } else { + if logger != nil { + logger.Error("[ExternalAssetHandler] Proxy error: %v", err) + } + rw.WriteHeader(http.StatusBadGateway) + } + } + + var result http.Handler = http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet { + proxy.ServeHTTP(rw, req) + return + } + + if baseHandler != nil { + baseHandler.ServeHTTP(rw, req) + return + } + + rw.WriteHeader(http.StatusMethodNotAllowed) + }) + + if middleware := options.Middleware; middleware != nil { + result = middleware(result) + } + + return result +} diff --git a/v2/pkg/assetserver/assetserver.go b/v2/pkg/assetserver/assetserver.go new file mode 100644 index 000000000..59665c091 --- /dev/null +++ b/v2/pkg/assetserver/assetserver.go @@ -0,0 +1,255 @@ +package assetserver + +import ( + "bytes" + "fmt" + "math/rand" + "net/http" + "strings" + + "golang.org/x/net/html" + "html/template" + + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +const ( + runtimeJSPath = "/wails/runtime.js" + ipcJSPath = "/wails/ipc.js" + runtimePath = "/wails/runtime" +) + +type RuntimeAssets interface { + DesktopIPC() []byte + WebsocketIPC() []byte + RuntimeDesktopJS() []byte +} + +type RuntimeHandler interface { + HandleRuntimeCall(w http.ResponseWriter, r *http.Request) +} + +type AssetServer struct { + handler http.Handler + runtimeJS []byte + ipcJS func(*http.Request) []byte + + logger Logger + runtime RuntimeAssets + + servingFromDisk bool + appendSpinnerToBody bool + + // Use http based runtime + runtimeHandler RuntimeHandler + + // plugin scripts + pluginScripts map[string]string + + assetServerWebView +} + +func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + assetOptions, err := BuildAssetServerConfig(options) + if err != nil { + return nil, err + } + return NewAssetServer(bindingsJSON, assetOptions, servingFromDisk, logger, runtime) +} + +func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + handler, err := NewAssetHandler(options, logger) + if err != nil { + return nil, err + } + + return NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime) +} + +func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + + var buffer bytes.Buffer + if bindingsJSON != "" { + escapedBindingsJSON := template.JSEscapeString(bindingsJSON) + buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n") + } + buffer.Write(runtime.RuntimeDesktopJS()) + + result := &AssetServer{ + handler: handler, + runtimeJS: buffer.Bytes(), + + // Check if we have been given a directory to serve assets from. + // If so, this means we are in dev mode and are serving assets off disk. + // We indicate this through the `servingFromDisk` flag to ensure requests + // aren't cached in dev mode. + servingFromDisk: servingFromDisk, + logger: logger, + runtime: runtime, + } + + return result, nil +} + +func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) { + d.runtimeHandler = handler +} + +func (d *AssetServer) AddPluginScript(pluginName string, script string) { + if d.pluginScripts == nil { + d.pluginScripts = make(map[string]string) + } + pluginName = strings.ReplaceAll(pluginName, "/", "_") + pluginName = html.EscapeString(pluginName) + pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000)) + d.pluginScripts[pluginScriptName] = script +} + +func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if isWebSocket(req) { + // WebSockets are not supported by the AssetServer + rw.WriteHeader(http.StatusNotImplemented) + return + } + + if d.servingFromDisk { + rw.Header().Add(HeaderCacheControl, "no-cache") + } + + handler := d.handler + if req.Method != http.MethodGet { + handler.ServeHTTP(rw, req) + return + } + + path := req.URL.Path + if path == runtimeJSPath { + d.writeBlob(rw, path, d.runtimeJS) + } else if path == runtimePath && d.runtimeHandler != nil { + d.runtimeHandler.HandleRuntimeCall(rw, req) + } else if path == ipcJSPath { + content := d.runtime.DesktopIPC() + if d.ipcJS != nil { + content = d.ipcJS(req) + } + d.writeBlob(rw, path, content) + + } else if script, ok := d.pluginScripts[path]; ok { + d.writeBlob(rw, path, []byte(script)) + } else if d.isRuntimeInjectionMatch(path) { + recorder := &bodyRecorder{ + ResponseWriter: rw, + doRecord: func(code int, h http.Header) bool { + if code == http.StatusNotFound { + return true + } + + if code != http.StatusOK { + return false + } + + return strings.Contains(h.Get(HeaderContentType), "text/html") + }, + } + + handler.ServeHTTP(recorder, req) + + body := recorder.Body() + if body == nil { + // The body has been streamed and not recorded, we are finished + return + } + + code := recorder.Code() + switch code { + case http.StatusOK: + content, err := d.processIndexHTML(body.Bytes()) + if err != nil { + d.serveError(rw, err, "Unable to processIndexHTML") + return + } + d.writeBlob(rw, indexHTML, content) + + case http.StatusNotFound: + d.writeBlob(rw, indexHTML, defaultHTML) + + default: + rw.WriteHeader(code) + + } + + } else { + handler.ServeHTTP(rw, req) + } +} + +func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) { + htmlNode, err := getHTMLNode(indexHTML) + if err != nil { + return nil, err + } + + if d.appendSpinnerToBody { + err = appendSpinnerToBody(htmlNode) + if err != nil { + return nil, err + } + } + + if err := insertScriptInHead(htmlNode, runtimeJSPath); err != nil { + return nil, err + } + + if err := insertScriptInHead(htmlNode, ipcJSPath); err != nil { + return nil, err + } + + // Inject plugins + for scriptName := range d.pluginScripts { + if err := insertScriptInHead(htmlNode, scriptName); err != nil { + return nil, err + } + } + + var buffer bytes.Buffer + err = html.Render(&buffer, htmlNode) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) { + err := serveFile(rw, filename, blob) + if err != nil { + d.serveError(rw, err, "Unable to write content %s", filename) + } +} + +func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) { + args = append(args, err) + d.logError(msg+": %s", args...) + rw.WriteHeader(http.StatusInternalServerError) +} + +func (d *AssetServer) logDebug(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Debug("[AssetServer] "+message, args...) + } +} + +func (d *AssetServer) logError(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Error("[AssetServer] "+message, args...) + } +} + +func (AssetServer) isRuntimeInjectionMatch(path string) bool { + if path == "" { + path = "/" + } + + return strings.HasSuffix(path, "/") || + strings.HasSuffix(path, "/"+indexHTML) +} diff --git a/v2/pkg/assetserver/assetserver_dev.go b/v2/pkg/assetserver/assetserver_dev.go new file mode 100644 index 000000000..f6a2a0d2f --- /dev/null +++ b/v2/pkg/assetserver/assetserver_dev.go @@ -0,0 +1,31 @@ +//go:build dev +// +build dev + +package assetserver + +import ( + "net/http" + "strings" +) + +/* +The assetserver for the dev mode. +Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The +default desktop IPC is injected when the webview accesses the devserver. +*/ +func NewDevAssetServer(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + result, err := NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime) + if err != nil { + return nil, err + } + + result.appendSpinnerToBody = true + result.ipcJS = func(req *http.Request) []byte { + if strings.Contains(req.UserAgent(), WailsUserAgentValue) { + return runtime.DesktopIPC() + } + return runtime.WebsocketIPC() + } + + return result, nil +} diff --git a/v2/pkg/assetserver/assetserver_webview.go b/v2/pkg/assetserver/assetserver_webview.go new file mode 100644 index 000000000..63f80f0ae --- /dev/null +++ b/v2/pkg/assetserver/assetserver_webview.go @@ -0,0 +1,185 @@ +package assetserver + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +type assetServerWebView struct { + // ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed. + ExpectedWebViewHost string + + dispatchInit sync.Once + dispatchReqC chan<- webview.Request + dispatchWorkers int +} + +// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server. +// The request will be finished with a StatusNotImplemented code if no handler has written to the response. +// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way. +func (d *AssetServer) ServeWebViewRequest(req webview.Request) { + d.dispatchInit.Do(func() { + workers := d.dispatchWorkers + if workers <= 0 { + return + } + + workerC := make(chan webview.Request, workers*2) + for i := 0; i < workers; i++ { + go func() { + for req := range workerC { + d.processWebViewRequest(req) + } + }() + } + + dispatchC := make(chan webview.Request) + go queueingDispatcher(50, dispatchC, workerC) + + d.dispatchReqC = dispatchC + }) + + if d.dispatchReqC == nil { + go d.processWebViewRequest(req) + } else { + d.dispatchReqC <- req + } +} + +func (d *AssetServer) processWebViewRequest(r webview.Request) { + uri, _ := r.URL() + d.processWebViewRequestInternal(r) + if err := r.Close(); err != nil { + d.logError("Unable to call close for request for uri '%s'", uri) + } +} + +// processWebViewRequestInternal processes the HTTP Request by faking a golang HTTP Server. +// The request will be finished with a StatusNotImplemented code if no handler has written to the response. +func (d *AssetServer) processWebViewRequestInternal(r webview.Request) { + uri := "unknown" + var err error + + wrw := r.Response() + defer func() { + if err := wrw.Finish(); err != nil { + d.logError("Error finishing request '%s': %s", uri, err) + } + }() + + var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer + defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status + + uri, err = r.URL() + if err != nil { + d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + method, err := r.Method() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err)) + return + } + + header, err := r.Header() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err)) + return + } + + body, err := r.Body() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err)) + return + } + + if body == nil { + body = http.NoBody + } + defer body.Close() + + req, err := http.NewRequest(method, uri, body) + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err)) + return + } + + // For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For + // most requests, fields other than Path and RawQuery will be empty. (See RFC 7230, Section 5.3) + req.URL.Scheme = "" + req.URL.Host = "" + req.URL.Fragment = "" + req.URL.RawFragment = "" + + if url := req.URL; req.RequestURI == "" && url != nil { + req.RequestURI = url.String() + } + + req.Header = header + + if req.RemoteAddr == "" { + // 192.0.2.0/24 is "TEST-NET" in RFC 5737 + req.RemoteAddr = "192.0.2.1:1234" + } + + if req.ContentLength == 0 { + req.ContentLength = -1 + } else { + size := strconv.FormatInt(req.ContentLength, 10) + req.Header.Set(HeaderContentLength, size) + } + + if host := req.Header.Get(HeaderHost); host != "" { + req.Host = host + } + + if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host)) + return + } + + d.ServeHTTP(rw, req) +} + +func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) { + logInfo := uri + if uri, err := url.ParseRequestURI(uri); err == nil { + logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1) + } + + d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err) + http.Error(rw, err.Error(), http.StatusInternalServerError) +} + +func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) { + q := newRingqueue[T](minQueueSize) + for { + in, ok := <-inC + if !ok { + return + } + + q.Add(in) + for q.Len() != 0 { + out, _ := q.Peek() + select { + case outC <- out: + q.Remove() + case in, ok := <-inC: + if !ok { + return + } + + q.Add(in) + } + } + } +} diff --git a/v2/pkg/assetserver/body_recorder.go b/v2/pkg/assetserver/body_recorder.go new file mode 100644 index 000000000..fa3bc1e7c --- /dev/null +++ b/v2/pkg/assetserver/body_recorder.go @@ -0,0 +1,61 @@ +package assetserver + +import ( + "bytes" + "net/http" +) + +type bodyRecorder struct { + http.ResponseWriter + doRecord func(code int, header http.Header) bool + + body *bytes.Buffer + code int + wroteHeader bool +} + +func (rw *bodyRecorder) Write(buf []byte) (int, error) { + rw.writeHeader(buf, http.StatusOK) + if rw.body != nil { + return rw.body.Write(buf) + } + return rw.ResponseWriter.Write(buf) +} + +func (rw *bodyRecorder) WriteHeader(code int) { + rw.writeHeader(nil, code) +} + +func (rw *bodyRecorder) Code() int { + return rw.code +} + +func (rw *bodyRecorder) Body() *bytes.Buffer { + return rw.body +} + +func (rw *bodyRecorder) writeHeader(buf []byte, code int) { + if rw.wroteHeader { + return + } + + if rw.doRecord != nil { + header := rw.Header() + if len(buf) != 0 { + if _, hasType := header[HeaderContentType]; !hasType { + header.Set(HeaderContentType, http.DetectContentType(buf)) + } + } + + if rw.doRecord(code, header) { + rw.body = bytes.NewBuffer(nil) + } + } + + if rw.body == nil { + rw.ResponseWriter.WriteHeader(code) + } + + rw.code = code + rw.wroteHeader = true +} diff --git a/v2/pkg/assetserver/common.go b/v2/pkg/assetserver/common.go new file mode 100644 index 000000000..57934e08e --- /dev/null +++ b/v2/pkg/assetserver/common.go @@ -0,0 +1,135 @@ +package assetserver + +import ( + "bytes" + "errors" + "io" + "net/http" + "strconv" + "strings" + + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "golang.org/x/net/html" +) + +func BuildAssetServerConfig(appOptions *options.App) (assetserver.Options, error) { + var options assetserver.Options + if opt := appOptions.AssetServer; opt != nil { + if appOptions.Assets != nil || appOptions.AssetsHandler != nil { + panic("It's not possible to use the deprecated Assets and AssetsHandler options and the new AssetServer option at the same time. Please migrate all your Assets options to the AssetServer option.") + } + + options = *opt + } else { + options = assetserver.Options{ + Assets: appOptions.Assets, + Handler: appOptions.AssetsHandler, + } + } + + return options, options.Validate() +} + +const ( + HeaderHost = "Host" + HeaderContentType = "Content-Type" + HeaderContentLength = "Content-Length" + HeaderUserAgent = "User-Agent" + HeaderCacheControl = "Cache-Control" + HeaderUpgrade = "Upgrade" + + WailsUserAgentValue = "wails.io" +) + +func serveFile(rw http.ResponseWriter, filename string, blob []byte) error { + header := rw.Header() + header.Set(HeaderContentLength, strconv.Itoa(len(blob))) + if mimeType := header.Get(HeaderContentType); mimeType == "" { + mimeType = GetMimetype(filename, blob) + header.Set(HeaderContentType, mimeType) + } + + rw.WriteHeader(http.StatusOK) + _, err := io.Copy(rw, bytes.NewReader(blob)) + return err +} + +func createScriptNode(scriptName string) *html.Node { + return &html.Node{ + Type: html.ElementNode, + Data: "script", + Attr: []html.Attribute{ + { + Key: "src", + Val: scriptName, + }, + }, + } +} + +func createDivNode(id string) *html.Node { + return &html.Node{ + Type: html.ElementNode, + Data: "div", + Attr: []html.Attribute{ + { + Namespace: "", + Key: "id", + Val: id, + }, + }, + } +} + +func insertScriptInHead(htmlNode *html.Node, scriptName string) error { + headNode := findFirstTag(htmlNode, "head") + if headNode == nil { + return errors.New("cannot find head in HTML") + } + scriptNode := createScriptNode(scriptName) + if headNode.FirstChild != nil { + headNode.InsertBefore(scriptNode, headNode.FirstChild) + } else { + headNode.AppendChild(scriptNode) + } + return nil +} + +func appendSpinnerToBody(htmlNode *html.Node) error { + bodyNode := findFirstTag(htmlNode, "body") + if bodyNode == nil { + return errors.New("cannot find body in HTML") + } + scriptNode := createDivNode("wails-spinner") + bodyNode.AppendChild(scriptNode) + return nil +} + +func getHTMLNode(htmldata []byte) (*html.Node, error) { + return html.Parse(bytes.NewReader(htmldata)) +} + +func findFirstTag(htmlnode *html.Node, tagName string) *html.Node { + var extractor func(*html.Node) *html.Node + var result *html.Node + extractor = func(node *html.Node) *html.Node { + if node.Type == html.ElementNode && node.Data == tagName { + return node + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + result := extractor(child) + if result != nil { + return result + } + } + return nil + } + result = extractor(htmlnode) + return result +} + +func isWebSocket(req *http.Request) bool { + upgrade := req.Header.Get(HeaderUpgrade) + return strings.EqualFold(upgrade, "websocket") +} diff --git a/v2/pkg/assetserver/content_type_sniffer.go b/v2/pkg/assetserver/content_type_sniffer.go new file mode 100644 index 000000000..475428ae5 --- /dev/null +++ b/v2/pkg/assetserver/content_type_sniffer.go @@ -0,0 +1,42 @@ +package assetserver + +import ( + "net/http" +) + +type contentTypeSniffer struct { + rw http.ResponseWriter + + wroteHeader bool +} + +func (rw *contentTypeSniffer) Header() http.Header { + return rw.rw.Header() +} + +func (rw *contentTypeSniffer) Write(buf []byte) (int, error) { + rw.writeHeader(buf) + return rw.rw.Write(buf) +} + +func (rw *contentTypeSniffer) WriteHeader(code int) { + if rw.wroteHeader { + return + } + + rw.rw.WriteHeader(code) + rw.wroteHeader = true +} + +func (rw *contentTypeSniffer) writeHeader(b []byte) { + if rw.wroteHeader { + return + } + + m := rw.rw.Header() + if _, hasType := m[HeaderContentType]; !hasType { + m.Set(HeaderContentType, http.DetectContentType(b)) + } + + rw.WriteHeader(http.StatusOK) +} diff --git a/v2/pkg/assetserver/defaultindex.html b/v2/pkg/assetserver/defaultindex.html new file mode 100644 index 000000000..1ea97c405 --- /dev/null +++ b/v2/pkg/assetserver/defaultindex.html @@ -0,0 +1,39 @@ + + + + + index.html not found + + + + +
index.html not found
+

Please try reloading the page

+ + \ No newline at end of file diff --git a/v2/pkg/assetserver/fs.go b/v2/pkg/assetserver/fs.go new file mode 100644 index 000000000..7ecc9cec8 --- /dev/null +++ b/v2/pkg/assetserver/fs.go @@ -0,0 +1,75 @@ +package assetserver + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files. +func FindEmbedRootPath(fsys embed.FS) (string, error) { + stopErr := fmt.Errorf("files or multiple dirs found") + + fPath := "" + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + fPath = path + if entries, dErr := fs.ReadDir(fsys, path); dErr != nil { + return dErr + } else if len(entries) <= 1 { + return nil + } + } + + return stopErr + }) + + if err != nil && err != stopErr { + return "", err + } + + return fPath, nil +} + +func FindPathToFile(fsys fs.FS, file string) (string, error) { + stat, _ := fs.Stat(fsys, file) + if stat != nil { + return ".", nil + } + var indexFiles []string + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(path, file) { + indexFiles = append(indexFiles, path) + } + return nil + }) + if err != nil { + return "", err + } + + if len(indexFiles) > 1 { + selected := indexFiles[0] + for _, f := range indexFiles { + if len(f) < len(selected) { + selected = f + } + } + path, _ := filepath.Split(selected) + return path, nil + } + if len(indexFiles) > 0 { + path, _ := filepath.Split(indexFiles[0]) + return path, nil + } + return "", fmt.Errorf("%s: %w", file, os.ErrNotExist) +} diff --git a/v2/pkg/assetserver/mimecache.go b/v2/pkg/assetserver/mimecache.go new file mode 100644 index 000000000..9d97e8f5a --- /dev/null +++ b/v2/pkg/assetserver/mimecache.go @@ -0,0 +1,67 @@ +package assetserver + +import ( + "net/http" + "path/filepath" + "sync" + + "github.com/wailsapp/mimetype" +) + +var ( + mimeCache = map[string]string{} + mimeMutex sync.Mutex + + // The list of builtin mime-types by extension as defined by + // the golang standard lib package "mime" + // The standard lib also takes into account mime type definitions from + // etc files like '/etc/apache2/mime.types' but we want to have the + // same behavivour on all platforms and not depend on some external file. + mimeTypesByExt = map[string]string{ + ".avif": "image/avif", + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".mjs": "text/javascript; charset=utf-8", + ".pdf": "application/pdf", + ".png": "image/png", + ".svg": "image/svg+xml", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".xml": "text/xml; charset=utf-8", + } +) + +func GetMimetype(filename string, data []byte) string { + mimeMutex.Lock() + defer mimeMutex.Unlock() + + result := mimeTypesByExt[filepath.Ext(filename)] + if result != "" { + return result + } + + result = mimeCache[filename] + if result != "" { + return result + } + + detect := mimetype.Detect(data) + if detect == nil { + result = http.DetectContentType(data) + } else { + result = detect.String() + } + + if result == "" { + result = "application/octet-stream" + } + + mimeCache[filename] = result + return result +} diff --git a/v2/pkg/assetserver/mimecache_test.go b/v2/pkg/assetserver/mimecache_test.go new file mode 100644 index 000000000..1496dbf52 --- /dev/null +++ b/v2/pkg/assetserver/mimecache_test.go @@ -0,0 +1,47 @@ +package assetserver + +import ( + "testing" +) + +func TestGetMimetype(t *testing.T) { + type args struct { + filename string + data []byte + } + bomUTF8 := []byte{0xef, 0xbb, 0xbf} + var emptyMsg []byte + css := []byte("body{margin:0;padding:0;background-color:#d579b2}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;background-color:#ededed}#nav{padding:30px}#nav a{font-weight:700;color:#2c\n3e50}#nav a.router-link-exact-active{color:#42b983}.hello[data-v-4e26ad49]{margin:10px 0}") + html := []byte("title") + bomHtml := append(bomUTF8, html...) + svg := []byte("") + svgWithComment := append([]byte(""), svg...) + svgWithCommentAndControlChars := append([]byte(" \r\n "), svgWithComment...) + svgWithBomCommentAndControlChars := append(bomUTF8, append([]byte(" \r\n "), svgWithComment...)...) + + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + {"nil data", args{"nil.svg", nil}, "image/svg+xml"}, + {"empty data", args{"empty.html", emptyMsg}, "text/html; charset=utf-8"}, + {"css", args{"test.css", css}, "text/css; charset=utf-8"}, + {"js", args{"test.js", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"}, + {"mjs", args{"test.mjs", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"}, + {"html-utf8", args{"test_utf8.html", html}, "text/html; charset=utf-8"}, + {"html-bom-utf8", args{"test_bom_utf8.html", bomHtml}, "text/html; charset=utf-8"}, + {"svg", args{"test.svg", svg}, "image/svg+xml"}, + {"svg-w-comment", args{"test_comment.svg", svgWithComment}, "image/svg+xml"}, + {"svg-w-control-comment", args{"test_control_comment.svg", svgWithCommentAndControlChars}, "image/svg+xml"}, + {"svg-w-bom-control-comment", args{"test_bom_control_comment.svg", svgWithBomCommentAndControlChars}, "image/svg+xml"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetMimetype(tt.args.filename, tt.args.data); got != tt.want { + t.Errorf("GetMimetype() = '%v', want '%v'", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/assetserver/ringqueue.go b/v2/pkg/assetserver/ringqueue.go new file mode 100644 index 000000000..b94e7cd5c --- /dev/null +++ b/v2/pkg/assetserver/ringqueue.go @@ -0,0 +1,101 @@ +// Code from https://github.com/erikdubbelboer/ringqueue +/* +The MIT License (MIT) + +Copyright (c) 2015 Erik Dubbelboer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package assetserver + +type ringqueue[T any] struct { + nodes []T + head int + tail int + cnt int + + minSize int +} + +func newRingqueue[T any](minSize uint) *ringqueue[T] { + if minSize < 2 { + minSize = 2 + } + return &ringqueue[T]{ + nodes: make([]T, minSize), + minSize: int(minSize), + } +} + +func (q *ringqueue[T]) resize(n int) { + nodes := make([]T, n) + if q.head < q.tail { + copy(nodes, q.nodes[q.head:q.tail]) + } else { + copy(nodes, q.nodes[q.head:]) + copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail]) + } + + q.tail = q.cnt % n + q.head = 0 + q.nodes = nodes +} + +func (q *ringqueue[T]) Add(i T) { + if q.cnt == len(q.nodes) { + // Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy + // In Go this resulted in a higher memory usage. + q.resize(q.cnt * 2) + } + q.nodes[q.tail] = i + q.tail = (q.tail + 1) % len(q.nodes) + q.cnt++ +} + +func (q *ringqueue[T]) Peek() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + return q.nodes[q.head], true +} + +func (q *ringqueue[T]) Remove() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + i := q.nodes[q.head] + q.head = (q.head + 1) % len(q.nodes) + q.cnt-- + + if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n { + q.resize(n) + } + + return i, true +} + +func (q *ringqueue[T]) Cap() int { + return cap(q.nodes) +} + +func (q *ringqueue[T]) Len() int { + return q.cnt +} diff --git a/v2/pkg/assetserver/testdata/index.html b/v2/pkg/assetserver/testdata/index.html new file mode 100644 index 000000000..76da518f4 --- /dev/null +++ b/v2/pkg/assetserver/testdata/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/v2/pkg/assetserver/testdata/main.css b/v2/pkg/assetserver/testdata/main.css new file mode 100644 index 000000000..57b00e6c6 --- /dev/null +++ b/v2/pkg/assetserver/testdata/main.css @@ -0,0 +1,39 @@ + +html { + text-align: center; + color: white; + background-color: rgba(1, 1, 1, 0.1); +} + +body { + color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + margin: 0; +} + +#result { + margin-top: 1rem; +} + +button { + -webkit-appearance: default-button; + padding: 6px; +} + +#name { + border-radius: 3px; + outline: none; + height: 20px; + -webkit-font-smoothing: antialiased; +} + +#logo { + width: 40%; + height: 40%; + padding-top: 20%; + margin: auto; + display: block; + background-position: center; + background-repeat: no-repeat; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg=="); +} diff --git a/v2/pkg/assetserver/testdata/main.js b/v2/pkg/assetserver/testdata/main.js new file mode 100644 index 000000000..274b4667c --- /dev/null +++ b/v2/pkg/assetserver/testdata/main.js @@ -0,0 +1,20 @@ +import {ready} from '@wails/runtime'; + +ready(() => { + // Get input + focus + let nameElement = document.getElementById("name"); + nameElement.focus(); + + // Setup the greet function + window.greet = function () { + + // Get name + let name = nameElement.value; + + // Call App.Greet(name) + window.backend.main.App.Greet(name).then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }); + }; +}); \ No newline at end of file diff --git a/v2/pkg/assetserver/testdata/subdir/index.html b/v2/pkg/assetserver/testdata/subdir/index.html new file mode 100644 index 000000000..76da518f4 --- /dev/null +++ b/v2/pkg/assetserver/testdata/subdir/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/v2/pkg/assetserver/testdata/subdir/main.css b/v2/pkg/assetserver/testdata/subdir/main.css new file mode 100644 index 000000000..57b00e6c6 --- /dev/null +++ b/v2/pkg/assetserver/testdata/subdir/main.css @@ -0,0 +1,39 @@ + +html { + text-align: center; + color: white; + background-color: rgba(1, 1, 1, 0.1); +} + +body { + color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + margin: 0; +} + +#result { + margin-top: 1rem; +} + +button { + -webkit-appearance: default-button; + padding: 6px; +} + +#name { + border-radius: 3px; + outline: none; + height: 20px; + -webkit-font-smoothing: antialiased; +} + +#logo { + width: 40%; + height: 40%; + padding-top: 20%; + margin: auto; + display: block; + background-position: center; + background-repeat: no-repeat; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg=="); +} diff --git a/v2/pkg/assetserver/testdata/subdir/main.js b/v2/pkg/assetserver/testdata/subdir/main.js new file mode 100644 index 000000000..274b4667c --- /dev/null +++ b/v2/pkg/assetserver/testdata/subdir/main.js @@ -0,0 +1,20 @@ +import {ready} from '@wails/runtime'; + +ready(() => { + // Get input + focus + let nameElement = document.getElementById("name"); + nameElement.focus(); + + // Setup the greet function + window.greet = function () { + + // Get name + let name = nameElement.value; + + // Call App.Greet(name) + window.backend.main.App.Greet(name).then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }); + }; +}); \ No newline at end of file diff --git a/v2/pkg/assetserver/testdata/testdata.go b/v2/pkg/assetserver/testdata/testdata.go new file mode 100644 index 000000000..5387070ec --- /dev/null +++ b/v2/pkg/assetserver/testdata/testdata.go @@ -0,0 +1,6 @@ +package testdata + +import "embed" + +//go:embed index.html main.css main.js +var TopLevelFS embed.FS diff --git a/v2/pkg/assetserver/webview/request.go b/v2/pkg/assetserver/webview/request.go new file mode 100644 index 000000000..18ff29890 --- /dev/null +++ b/v2/pkg/assetserver/webview/request.go @@ -0,0 +1,17 @@ +package webview + +import ( + "io" + "net/http" +) + +type Request interface { + URL() (string, error) + Method() (string, error) + Header() (http.Header, error) + Body() (io.ReadCloser, error) + + Response() ResponseWriter + + Close() error +} diff --git a/v2/pkg/assetserver/webview/request_darwin.go b/v2/pkg/assetserver/webview/request_darwin.go new file mode 100644 index 000000000..c44e5f196 --- /dev/null +++ b/v2/pkg/assetserver/webview/request_darwin.go @@ -0,0 +1,251 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import +#include + +static void URLSchemeTaskRetain(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask retain]; +} + +static void URLSchemeTaskRelease(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask release]; +} + +static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.URL.absoluteString UTF8String]; + } +} + +static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.HTTPMethod UTF8String]; + } +} + +static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil]; + if (!headerData) { + return nil; + } + + NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease]; + const char * headerJSON = [headerString UTF8String]; + + return strdup(headerJSON); + } +} + +static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBody) { + return false; + } + + *body = urlSchemeTask.request.HTTPBody.bytes; + *bodyLen = urlSchemeTask.request.HTTPBody.length; + return true; + } +} + +static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return false; + } + + [urlSchemeTask.request.HTTPBodyStream open]; + return true; + } +} + +static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return; + } + + [urlSchemeTask.request.HTTPBodyStream close]; + } +} + +static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + + @autoreleasepool { + NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; + if (!stream) { + return -2; + } + + NSStreamStatus status = stream.streamStatus; + if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) { + return 0; + } else if (status != NSStreamStatusOpen) { + return -3; + } + + return [stream read:buf maxLength:bufLen]; + } +} +*/ +import "C" + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "unsafe" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `id` +func NewRequest(wkURLSchemeTask unsafe.Pointer) Request { + C.URLSchemeTaskRetain(wkURLSchemeTask) + return newRequestFinalizer(&request{task: wkURLSchemeTask}) +} + +var _ Request = &request{} + +type request struct { + task unsafe.Pointer + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) URL() (string, error) { + return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil +} + +func (r *request) Method() (string, error) { + return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + header := http.Header{} + if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil { + if headers := C.GoString(cHeaders); headers != "" { + var h map[string]string + if err := json.Unmarshal([]byte(headers), &h); err != nil { + return nil, fmt.Errorf("unable to unmarshal request headers: %s", err) + } + + for k, v := range h { + header.Add(k, v) + } + } + C.free(unsafe.Pointer(cHeaders)) + } + r.header = header + return header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + var body unsafe.Pointer + var bodyLen C.int + if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) { + if body != nil && bodyLen > 0 { + r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen))) + } else { + r.body = http.NoBody + } + } else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) { + r.body = &requestBodyStreamReader{task: r.task} + } + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{r: r} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + err = r.Response().Finish() + if err != nil { + return err + } + C.URLSchemeTaskRelease(r.task) + return err +} + +var _ io.ReadCloser = &requestBodyStreamReader{} + +type requestBodyStreamReader struct { + task unsafe.Pointer + closed bool +} + +// Read implements io.Reader +func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) { + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen)) + if res > 0 { + return int(res), nil + } + + switch res { + case 0: + return 0, io.EOF + case -1: + return 0, fmt.Errorf("body: stream error") + case -2: + return 0, fmt.Errorf("body: no stream defined") + case -3: + return 0, io.ErrClosedPipe + default: + return 0, fmt.Errorf("body: unknown error %d", res) + } +} + +func (r *requestBodyStreamReader) Close() error { + if r.closed { + return nil + } + r.closed = true + + C.URLSchemeTaskRequestBodyStreamClose(r.task) + return nil +} diff --git a/v2/pkg/assetserver/webview/request_finalizer.go b/v2/pkg/assetserver/webview/request_finalizer.go new file mode 100644 index 000000000..6a8c6a928 --- /dev/null +++ b/v2/pkg/assetserver/webview/request_finalizer.go @@ -0,0 +1,40 @@ +package webview + +import ( + "runtime" + "sync/atomic" +) + +var _ Request = &requestFinalizer{} + +type requestFinalizer struct { + Request + closed int32 +} + +// newRequestFinalizer returns a request with a runtime finalizer to make sure it will be closed from the finalizer +// if it has not been already closed. +// It also makes sure Close() of the wrapping request is only called once. +func newRequestFinalizer(r Request) Request { + rf := &requestFinalizer{Request: r} + // Make sure to async release since it might block the finalizer goroutine for a longer period + runtime.SetFinalizer(rf, func(obj *requestFinalizer) { rf.close(true) }) + return rf +} + +func (r *requestFinalizer) Close() error { + return r.close(false) +} + +func (r *requestFinalizer) close(asyncRelease bool) error { + if atomic.CompareAndSwapInt32(&r.closed, 0, 1) { + runtime.SetFinalizer(r, nil) + if asyncRelease { + go r.Request.Close() + return nil + } else { + return r.Request.Close() + } + } + return nil +} diff --git a/v2/pkg/assetserver/webview/request_linux.go b/v2/pkg/assetserver/webview/request_linux.go new file mode 100644 index 000000000..c6785fb1c --- /dev/null +++ b/v2/pkg/assetserver/webview/request_linux.go @@ -0,0 +1,85 @@ +//go:build linux +// +build linux + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "io" + "net/http" + "unsafe" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest` +func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request { + webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest) + C.g_object_ref(C.gpointer(webkitReq)) + + req := &request{req: webkitReq} + return newRequestFinalizer(req) +} + +var _ Request = &request{} + +type request struct { + req *C.WebKitURISchemeRequest + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) URL() (string, error) { + return C.GoString(C.webkit_uri_scheme_request_get_uri(r.req)), nil +} + +func (r *request) Method() (string, error) { + return webkit_uri_scheme_request_get_http_method(r.req), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + r.header = webkit_uri_scheme_request_get_http_headers(r.req) + return r.header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + r.body = webkit_uri_scheme_request_get_http_body(r.req) + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{req: r.req} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + r.Response().Finish() + C.g_object_unref(C.gpointer(r.req)) + return err +} diff --git a/v2/pkg/assetserver/webview/request_windows.go b/v2/pkg/assetserver/webview/request_windows.go new file mode 100644 index 000000000..fa83cd8d7 --- /dev/null +++ b/v2/pkg/assetserver/webview/request_windows.go @@ -0,0 +1,217 @@ +//go:build windows +// +build windows + +package webview + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/wailsapp/go-webview2/pkg/edge" +) + +// NewRequest creates as new WebViewRequest for chromium. This Method must be called from the Main-Thread! +func NewRequest(env *edge.ICoreWebView2Environment, args *edge.ICoreWebView2WebResourceRequestedEventArgs, invokeSync func(fn func())) (Request, error) { + req, err := args.GetRequest() + if err != nil { + return nil, fmt.Errorf("GetRequest failed: %s", err) + } + defer req.Release() + + r := &request{ + invokeSync: invokeSync, + } + + code := http.StatusInternalServerError + r.response, err = env.CreateWebResourceResponse(nil, code, http.StatusText(code), "") + if err != nil { + return nil, fmt.Errorf("CreateWebResourceResponse failed: %s", err) + } + + if err := args.PutResponse(r.response); err != nil { + r.finishResponse() + return nil, fmt.Errorf("PutResponse failed: %s", err) + } + + r.deferral, err = args.GetDeferral() + if err != nil { + r.finishResponse() + return nil, fmt.Errorf("GetDeferral failed: %s", err) + } + + r.url, r.urlErr = req.GetUri() + r.method, r.methodErr = req.GetMethod() + r.header, r.headerErr = getHeaders(req) + + if content, err := req.GetContent(); err != nil { + r.bodyErr = err + } else if content != nil { + // It is safe to access Content from another Thread: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#thread-safety + r.body = &iStreamReleaseCloser{stream: content} + } + + return r, nil +} + +var _ Request = &request{} + +type request struct { + response *edge.ICoreWebView2WebResourceResponse + deferral *edge.ICoreWebView2Deferral + + url string + urlErr error + + method string + methodErr error + + header http.Header + headerErr error + + body io.ReadCloser + bodyErr error + rw *responseWriter + + invokeSync func(fn func()) +} + +func (r *request) URL() (string, error) { + return r.url, r.urlErr +} + +func (r *request) Method() (string, error) { + return r.method, r.methodErr +} + +func (r *request) Header() (http.Header, error) { + return r.header, r.headerErr +} + +func (r *request) Body() (io.ReadCloser, error) { + return r.body, r.bodyErr +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{req: r} + return r.rw +} + +func (r *request) Close() error { + var errs []error + if r.body != nil { + if err := r.body.Close(); err != nil { + errs = append(errs, err) + } + r.body = nil + } + + if err := r.Response().Finish(); err != nil { + errs = append(errs, err) + } + + return combineErrs(errs) +} + +// finishResponse must be called on the main-thread +func (r *request) finishResponse() error { + var errs []error + if r.response != nil { + if err := r.response.Release(); err != nil { + errs = append(errs, err) + } + r.response = nil + } + if r.deferral != nil { + if err := r.deferral.Complete(); err != nil { + errs = append(errs, err) + } + + if err := r.deferral.Release(); err != nil { + errs = append(errs, err) + } + r.deferral = nil + } + return combineErrs(errs) +} + +type iStreamReleaseCloser struct { + stream *edge.IStream + closed bool +} + +func (i *iStreamReleaseCloser) Read(p []byte) (int, error) { + if i.closed { + return 0, io.ErrClosedPipe + } + return i.stream.Read(p) +} + +func (i *iStreamReleaseCloser) Close() error { + if i.closed { + return nil + } + i.closed = true + return i.stream.Release() +} + +func getHeaders(req *edge.ICoreWebView2WebResourceRequest) (http.Header, error) { + header := http.Header{} + headers, err := req.GetHeaders() + if err != nil { + return nil, fmt.Errorf("GetHeaders Error: %s", err) + } + defer headers.Release() + + headersIt, err := headers.GetIterator() + if err != nil { + return nil, fmt.Errorf("GetIterator Error: %s", err) + } + defer headersIt.Release() + + for { + has, err := headersIt.HasCurrentHeader() + if err != nil { + return nil, fmt.Errorf("HasCurrentHeader Error: %s", err) + } + if !has { + break + } + + name, value, err := headersIt.GetCurrentHeader() + if err != nil { + return nil, fmt.Errorf("GetCurrentHeader Error: %s", err) + } + + header.Set(name, value) + if _, err := headersIt.MoveNext(); err != nil { + return nil, fmt.Errorf("MoveNext Error: %s", err) + } + } + + // WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other + // requests including IPC calls. + // So prevent 304 status codes by removing the headers that are used in combinationwith caching. + header.Del("If-Modified-Since") + header.Del("If-None-Match") + return header, nil +} + +func combineErrs(errs []error) error { + // TODO use Go1.20 errors.Join + if len(errs) == 0 { + return nil + } + + errStrings := make([]string, len(errs)) + for i, err := range errs { + errStrings[i] = err.Error() + } + + return fmt.Errorf(strings.Join(errStrings, "\n")) +} diff --git a/v2/pkg/assetserver/webview/responsewriter.go b/v2/pkg/assetserver/webview/responsewriter.go new file mode 100644 index 000000000..dacbb567d --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter.go @@ -0,0 +1,25 @@ +package webview + +import ( + "errors" + "net/http" +) + +const ( + HeaderContentLength = "Content-Length" + HeaderContentType = "Content-Type" +) + +var ( + errRequestStopped = errors.New("request has been stopped") + errResponseFinished = errors.New("response has been finished") +) + +// A ResponseWriter interface is used by an HTTP handler to +// construct an HTTP response for the WebView. +type ResponseWriter interface { + http.ResponseWriter + + // Finish the response and flush all data. A Finish after the request has already been finished has no effect. + Finish() error +} diff --git a/v2/pkg/assetserver/webview/responsewriter_darwin.go b/v2/pkg/assetserver/webview/responsewriter_darwin.go new file mode 100644 index 000000000..a3c73b6f1 --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_darwin.go @@ -0,0 +1,164 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import + +typedef void (^schemeTaskCaller)(id); + +static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) { + id urlSchemeTask = (id) wkUrlSchemeTask; + if (urlSchemeTask == nil) { + return false; + } + + @autoreleasepool { + @try { + fn(urlSchemeTask); + } @catch (NSException *exception) { + // This is very bad to detect a stopped schemeTask this should be implemented in a better way + // But it seems to be very tricky to not deadlock when keeping a lock curing executing fn() + // It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want + // to get the lock again to start another request or stop it. + if ([exception.reason isEqualToString: @"This task has already been stopped"]) { + return false; + } + + @throw exception; + } + + return true; + } +} + +static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsdata = [NSData dataWithBytes:data length:datalength]; + [urlSchemeTask didReceiveData:nsdata]; + }); +} + +static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + [urlSchemeTask didFinish]; + }); +} + +static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength]; + NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil]; + NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease]; + + [urlSchemeTask didReceiveResponse:response]; + }); +} +*/ +import "C" + +import ( + "encoding/json" + "fmt" + "net/http" + "unsafe" +) + +var _ ResponseWriter = &responseWriter{} + +type responseWriter struct { + r *request + + header http.Header + wroteHeader bool + + finished bool +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + + var contentLen int + if buf != nil { + contentLen = len(buf) + } + + if contentLen > 0 { + // Create a C array to hold the data + cBuf := C.malloc(C.size_t(contentLen)) + if cBuf == nil { + return 0, fmt.Errorf("memory allocation failed for %d bytes", contentLen) + } + defer C.free(cBuf) + + // Copy the Go slice to the C array + C.memcpy(cBuf, unsafe.Pointer(&buf[0]), C.size_t(contentLen)) + + if !C.URLSchemeTaskDidReceiveData(rw.r.task, cBuf, C.int(contentLen)) { + return 0, errRequestStopped + } + } else { + if !C.URLSchemeTaskDidReceiveData(rw.r.task, nil, 0) { + return 0, errRequestStopped + } + } + + return contentLen, nil +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + header := map[string]string{} + for k := range rw.Header() { + header[k] = rw.Header().Get(k) + } + headerData, _ := json.Marshal(header) + + var headers unsafe.Pointer + var headersLen int + if len(headerData) != 0 { + headers = unsafe.Pointer(&headerData[0]) + headersLen = len(headerData) + } + + C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen)) +} + +func (rw *responseWriter) Finish() error { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return nil + } + rw.finished = true + + C.URLSchemeTaskDidFinish(rw.r.task) + return nil +} diff --git a/v2/pkg/assetserver/webview/responsewriter_linux.go b/v2/pkg/assetserver/webview/responsewriter_linux.go new file mode 100644 index 000000000..59646ce29 --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_linux.go @@ -0,0 +1,132 @@ +//go:build linux +// +build linux + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "gio/gunixinputstream.h" + +*/ +import "C" +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" + "syscall" + "unsafe" +) + +type responseWriter struct { + req *C.WebKitURISchemeRequest + + header http.Header + wroteHeader bool + finished bool + + w io.WriteCloser + wErr error +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + if rw.wErr != nil { + return 0, rw.wErr + } + return rw.w.Write(buf) +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + contentLength := int64(-1) + if sLen := rw.Header().Get(HeaderContentLength); sLen != "" { + if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { + contentLength = pLen + } + } + + // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the + // read FD is given to the InputStream and will be closed there. + // Furthermore we especially don't want to have the FD_CLOEXEC + rFD, w, err := pipe() + if err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) + return + } + rw.w = w + + stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1)) + defer C.g_object_unref(C.gpointer(stream)) + + if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + return + } +} + +func (rw *responseWriter) Finish() error { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return nil + } + rw.finished = true + if rw.w != nil { + rw.w.Close() + } + return nil +} + +func (rw *responseWriter) finishWithError(code int, err error) { + if rw.w != nil { + rw.w.Close() + rw.w = &nopCloser{io.Discard} + } + rw.wErr = err + + msg := C.CString(err.Error()) + gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg) + C.webkit_uri_scheme_request_finish_error(rw.req, gerr) + C.g_error_free(gerr) + C.free(unsafe.Pointer(msg)) +} + +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { return nil } + +func pipe() (r int, w *os.File, err error) { + var p [2]int + e := syscall.Pipe2(p[0:], 0) + if e != nil { + return 0, nil, fmt.Errorf("pipe2: %s", e) + } + + return p[0], os.NewFile(uintptr(p[1]), "|1"), nil +} diff --git a/v2/pkg/assetserver/webview/responsewriter_windows.go b/v2/pkg/assetserver/webview/responsewriter_windows.go new file mode 100644 index 000000000..748d9511b --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_windows.go @@ -0,0 +1,105 @@ +//go:build windows +// +build windows + +package webview + +import ( + "bytes" + "fmt" + "net/http" + "strings" +) + +var _ http.ResponseWriter = &responseWriter{} + +type responseWriter struct { + req *request + + header http.Header + wroteHeader bool + code int + body *bytes.Buffer + + finished bool +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + + return rw.body.Write(buf) +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + if rw.body == nil { + rw.body = &bytes.Buffer{} + } + + rw.code = code +} + +func (rw *responseWriter) Finish() error { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return nil + } + rw.finished = true + + var errs []error + + code := rw.code + if code == http.StatusNotModified { + // WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other + // requests including IPC calls. + errs = append(errs, fmt.Errorf("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError")) + code = http.StatusInternalServerError + } + + rw.req.invokeSync(func() { + resp := rw.req.response + + hdrs, err := resp.GetHeaders() + if err != nil { + errs = append(errs, fmt.Errorf("Resp.GetHeaders failed: %s", err)) + } else { + for k, v := range rw.header { + if err := hdrs.AppendHeader(k, strings.Join(v, ",")); err != nil { + errs = append(errs, fmt.Errorf("Resp.AppendHeader failed: %s", err)) + } + } + hdrs.Release() + } + + if err := resp.PutStatusCode(code); err != nil { + errs = append(errs, fmt.Errorf("Resp.PutStatusCode failed: %s", err)) + } + + if err := resp.PutByteContent(rw.body.Bytes()); err != nil { + errs = append(errs, fmt.Errorf("Resp.PutByteContent failed: %s", err)) + } + + if err := rw.req.finishResponse(); err != nil { + errs = append(errs, fmt.Errorf("Resp.finishResponse failed: %s", err)) + } + }) + + return combineErrs(errs) +} diff --git a/v2/pkg/assetserver/webview/webkit2_36+.go b/v2/pkg/assetserver/webview/webkit2_36+.go new file mode 100644 index 000000000..1f0db3c89 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_36+.go @@ -0,0 +1,71 @@ +//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41 ) + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 libsoup-2.4 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 libsoup-3.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "libsoup/soup.h" +*/ +import "C" + +import ( + "net/http" + "strings" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string { + method := C.GoString(C.webkit_uri_scheme_request_get_http_method(req)) + return strings.ToUpper(method) +} + +func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header { + hdrs := C.webkit_uri_scheme_request_get_http_headers(req) + + var iter C.SoupMessageHeadersIter + C.soup_message_headers_iter_init(&iter, hdrs) + + var name *C.char + var value *C.char + + h := http.Header{} + for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 { + h.Add(C.GoString(name), C.GoString(value)) + } + + return h +} + +func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error { + resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength)) + defer C.g_object_unref(C.gpointer(resp)) + + cReason := C.CString(http.StatusText(code)) + C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason) + C.free(unsafe.Pointer(cReason)) + + cMimeType := C.CString(header.Get(HeaderContentType)) + C.webkit_uri_scheme_response_set_content_type(resp, cMimeType) + C.free(unsafe.Pointer(cMimeType)) + + hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE) + for name, values := range header { + cName := C.CString(name) + for _, value := range values { + cValue := C.CString(value) + C.soup_message_headers_append(hdrs, cName, cValue) + C.free(unsafe.Pointer(cValue)) + } + C.free(unsafe.Pointer(cName)) + } + + C.webkit_uri_scheme_response_set_http_headers(resp, hdrs) + + C.webkit_uri_scheme_request_finish_with_response(req, resp) + return nil +} diff --git a/v2/pkg/assetserver/webview/webkit2_36.go b/v2/pkg/assetserver/webview/webkit2_36.go new file mode 100644 index 000000000..cd200af8e --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_36.go @@ -0,0 +1,21 @@ +//go:build linux && webkit2_36 + +package webview + +/* +#cgo linux pkg-config: webkit2gtk-4.0 + +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "io" + "net/http" +) + +const Webkit2MinMinorVersion = 36 + +func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser { + return http.NoBody +} diff --git a/v2/pkg/assetserver/webview/webkit2_40+.go b/v2/pkg/assetserver/webview/webkit2_40+.go new file mode 100644 index 000000000..eb3e439f2 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_40+.go @@ -0,0 +1,85 @@ +//go:build linux && (webkit2_40 || webkit2_41) + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "gio/gunixinputstream.h" +*/ +import "C" + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser { + stream := C.webkit_uri_scheme_request_get_http_body(req) + if stream == nil { + return http.NoBody + } + return &webkitRequestBody{stream: stream} +} + +type webkitRequestBody struct { + stream *C.GInputStream + closed bool +} + +// Read implements io.Reader +func (r *webkitRequestBody) Read(p []byte) (int, error) { + if r.closed { + return 0, io.ErrClosedPipe + } + + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + var n C.gsize + var gErr *C.GError + res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr) + if res == 0 { + return 0, formatGError("stream read failed", gErr) + } else if n == 0 { + return 0, io.EOF + } + return int(n), nil +} + +func (r *webkitRequestBody) Close() error { + if r.closed { + return nil + } + r.closed = true + + // https://docs.gtk.org/gio/method.InputStream.close.html + // Streams will be automatically closed when the last reference is dropped, but you might want to call this function + // to make sure resources are released as early as possible. + var err error + var gErr *C.GError + if C.g_input_stream_close(r.stream, nil, &gErr) == 0 { + err = formatGError("stream close failed", gErr) + } + C.g_object_unref(C.gpointer(r.stream)) + r.stream = nil + return err +} + +func formatGError(msg string, gErr *C.GError, args ...any) error { + if gErr != nil && gErr.message != nil { + msg += ": " + C.GoString(gErr.message) + C.g_error_free(gErr) + } + return fmt.Errorf(msg, args...) +} diff --git a/v2/pkg/assetserver/webview/webkit2_40.go b/v2/pkg/assetserver/webview/webkit2_40.go new file mode 100644 index 000000000..47b504383 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_40.go @@ -0,0 +1,5 @@ +//go:build linux && webkit2_40 + +package webview + +const Webkit2MinMinorVersion = 40 diff --git a/v2/pkg/assetserver/webview/webkit2_41.go b/v2/pkg/assetserver/webview/webkit2_41.go new file mode 100644 index 000000000..82f948d06 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_41.go @@ -0,0 +1,5 @@ +//go:build linux && webkit2_41 + +package webview + +const Webkit2MinMinorVersion = 41 diff --git a/v2/pkg/assetserver/webview/webkit2_legacy.go b/v2/pkg/assetserver/webview/webkit2_legacy.go new file mode 100644 index 000000000..1d1cf7c2b --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_legacy.go @@ -0,0 +1,48 @@ +//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41) + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +const Webkit2MinMinorVersion = 0 + +func webkit_uri_scheme_request_get_http_method(_ *C.WebKitURISchemeRequest) string { + return http.MethodGet +} + +func webkit_uri_scheme_request_get_http_headers(_ *C.WebKitURISchemeRequest) http.Header { + // Fake some basic default headers that are needed if e.g. request are being proxied to the an external sever, like + // we do in the devserver. + h := http.Header{} + h.Add("Accept", "*/*") + h.Add("User-Agent", "wails.io/605.1.15") + return h +} + +func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser { + return http.NoBody +} + +func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error { + if code != http.StatusOK { + return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code)) + } + + cMimeType := C.CString(header.Get(HeaderContentType)) + C.webkit_uri_scheme_request_finish(req, stream, C.gint64(streamLength), cMimeType) + C.free(unsafe.Pointer(cMimeType)) + return nil +} diff --git a/v2/pkg/buildassets/build/README.md b/v2/pkg/buildassets/build/README.md new file mode 100644 index 000000000..1ae2f677f --- /dev/null +++ b/v2/pkg/buildassets/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/v2/pkg/buildassets/build/appicon.png b/v2/pkg/buildassets/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v2/pkg/buildassets/build/appicon.png differ diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist new file mode 100644 index 000000000..14121ef7c --- /dev/null +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist new file mode 100644 index 000000000..d17a7475c --- /dev/null +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/v2/pkg/buildassets/build/windows/icon.ico b/v2/pkg/buildassets/build/windows/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/v2/pkg/buildassets/build/windows/icon.ico differ diff --git a/v2/pkg/buildassets/build/windows/info.json b/v2/pkg/buildassets/build/windows/info.json new file mode 100644 index 000000000..9727946b7 --- /dev/null +++ b/v2/pkg/buildassets/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi new file mode 100644 index 000000000..654ae2e49 --- /dev/null +++ b/v2/pkg/buildassets/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh new file mode 100644 index 000000000..2f6d32195 --- /dev/null +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/v2/pkg/buildassets/build/windows/wails.exe.manifest b/v2/pkg/buildassets/build/windows/wails.exe.manifest new file mode 100644 index 000000000..17e1a2387 --- /dev/null +++ b/v2/pkg/buildassets/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v2/pkg/buildassets/buildassets.go b/v2/pkg/buildassets/buildassets.go new file mode 100644 index 000000000..6934b98bd --- /dev/null +++ b/v2/pkg/buildassets/buildassets.go @@ -0,0 +1,142 @@ +package buildassets + +import ( + "bytes" + "embed" + "errors" + "fmt" + iofs "io/fs" + "os" + "path/filepath" + "text/template" + + "github.com/leaanthony/gosod" + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/project" +) + +//go:embed build +var assets embed.FS + +// Same as assets but chrooted into /build/ +var buildAssets iofs.FS + +func init() { + buildAssets = lo.Must(iofs.Sub(assets, "build")) +} + +// Install will install all default project assets +func Install(targetDir string) error { + templateDir := gosod.New(assets) + err := templateDir.Extract(targetDir, nil) + if err != nil { + return err + } + + return nil +} + +// GetLocalPath returns the local path of the requested build asset file +func GetLocalPath(projectData *project.Project, file string) string { + return filepath.Clean(filepath.Join(projectData.GetBuildDir(), filepath.FromSlash(file))) +} + +// ReadFile reads the file from the project build folder. +// If the file does not exist it falls back to the embedded file and the file will be written +// to the disk for customisation. +func ReadFile(projectData *project.Project, file string) ([]byte, error) { + localFilePath := GetLocalPath(projectData, file) + + content, err := os.ReadFile(localFilePath) + if errors.Is(err, iofs.ErrNotExist) { + // The file does not exist, let's read it from the assets FS and write it to disk + content, err := iofs.ReadFile(buildAssets, file) + if err != nil { + return nil, err + } + + if err := writeFileSystemFile(projectData, file, content); err != nil { + return nil, fmt.Errorf("Unable to create file in build folder: %s", err) + } + return content, nil + } + + return content, err +} + +// ReadFileWithProjectData reads the file from the project build folder and replaces ProjectInfo if necessary. +// If the file does not exist it falls back to the embedded file and the file will be written +// to the disk for customisation. The file written is the original unresolved one. +func ReadFileWithProjectData(projectData *project.Project, file string) ([]byte, error) { + content, err := ReadFile(projectData, file) + if err != nil { + return nil, err + } + + content, err = resolveProjectData(content, projectData) + if err != nil { + return nil, fmt.Errorf("Unable to resolve data in %s: %w", file, err) + } + return content, nil +} + +// ReadOriginalFileWithProjectDataAndSave reads the file from the embedded assets and replaces +// ProjectInfo if necessary. +// It will also write the resolved final file back to the project build folder. +func ReadOriginalFileWithProjectDataAndSave(projectData *project.Project, file string) ([]byte, error) { + content, err := iofs.ReadFile(buildAssets, file) + if err != nil { + return nil, fmt.Errorf("Unable to read file %s: %w", file, err) + } + + content, err = resolveProjectData(content, projectData) + if err != nil { + return nil, fmt.Errorf("Unable to resolve data in %s: %w", file, err) + } + + if err := writeFileSystemFile(projectData, file, content); err != nil { + return nil, fmt.Errorf("Unable to create file in build folder: %w", err) + } + return content, nil +} + +type assetData struct { + Name string + Info project.Info + OutputFilename string +} + +func resolveProjectData(content []byte, projectData *project.Project) ([]byte, error) { + tmpl, err := template.New("").Parse(string(content)) + if err != nil { + return nil, err + } + + data := &assetData{ + Name: projectData.Name, + Info: projectData.Info, + OutputFilename: projectData.OutputFilename, + } + + var out bytes.Buffer + if err := tmpl.Execute(&out, data); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func writeFileSystemFile(projectData *project.Project, file string, content []byte) error { + targetPath := GetLocalPath(projectData, file) + + if dir := filepath.Dir(targetPath); !fs.DirExists(dir) { + if err := fs.MkDirs(dir, 0o755); err != nil { + return fmt.Errorf("Unable to create directory: %w", err) + } + } + + if err := os.WriteFile(targetPath, content, 0o644); err != nil { + return err + } + return nil +} diff --git a/v2/pkg/buildassets/onhold/dialog/README.md b/v2/pkg/buildassets/onhold/dialog/README.md new file mode 100644 index 000000000..3b9189a8f --- /dev/null +++ b/v2/pkg/buildassets/onhold/dialog/README.md @@ -0,0 +1,29 @@ +## Dialog + +NOTE: Currently, this is a Mac only feature. + +Place any PNG file in this directory to be able to use them in message dialogs. +The files should have names in the following format: `name[-(light|dark)][2x].png` + +Examples: + +* `mypic.png` - Standard definition icon with ID `mypic` +* `mypic-light.png` - Standard definition icon with ID `mypic`, used when system theme is light +* `mypic-dark.png` - Standard definition icon with ID `mypic`, used when system theme is dark +* `mypic2x.png` - High definition icon with ID `mypic` +* `mypic-light2x.png` - High definition icon with ID `mypic`, used when system theme is light +* `mypic-dark2x.png` - High definition icon with ID `mypic`, used when system theme is dark + +### Order of preference + +Icons are selected with the following order of preference: + +For High Definition displays: +* name-(theme)2x.png +* name2x.png +* name-(theme).png +* name.png + +For Standard Definition displays: +* name-(theme).png +* name.png \ No newline at end of file diff --git a/v2/pkg/buildassets/onhold/dialog/info-dark.png b/v2/pkg/buildassets/onhold/dialog/info-dark.png new file mode 100644 index 000000000..9ff6655ee Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-dark.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/info-dark2x.png b/v2/pkg/buildassets/onhold/dialog/info-dark2x.png new file mode 100644 index 000000000..fcdf8006a Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-dark2x.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/info-light.png b/v2/pkg/buildassets/onhold/dialog/info-light.png new file mode 100644 index 000000000..1fb32e8a9 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-light.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/info-light2x.png b/v2/pkg/buildassets/onhold/dialog/info-light2x.png new file mode 100644 index 000000000..874b2d301 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-light2x.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/question-dark.png b/v2/pkg/buildassets/onhold/dialog/question-dark.png new file mode 100644 index 000000000..c2387420e Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-dark.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/question-dark2x.png b/v2/pkg/buildassets/onhold/dialog/question-dark2x.png new file mode 100644 index 000000000..86ea1b037 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-dark2x.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/question-light.png b/v2/pkg/buildassets/onhold/dialog/question-light.png new file mode 100644 index 000000000..0d3b6ba02 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-light.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/question-light2x.png b/v2/pkg/buildassets/onhold/dialog/question-light2x.png new file mode 100644 index 000000000..fcd21569f Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-light2x.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/warning-dark.png b/v2/pkg/buildassets/onhold/dialog/warning-dark.png new file mode 100644 index 000000000..db432321b Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-dark.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png b/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png new file mode 100644 index 000000000..3325d22d2 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/warning-light.png b/v2/pkg/buildassets/onhold/dialog/warning-light.png new file mode 100644 index 000000000..cf421a171 Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-light.png differ diff --git a/v2/pkg/buildassets/onhold/dialog/warning-light2x.png b/v2/pkg/buildassets/onhold/dialog/warning-light2x.png new file mode 100644 index 000000000..ff092f6cd Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-light2x.png differ diff --git a/v2/pkg/buildassets/onhold/tray/README.md b/v2/pkg/buildassets/onhold/tray/README.md new file mode 100644 index 000000000..5f4e7b4e6 --- /dev/null +++ b/v2/pkg/buildassets/onhold/tray/README.md @@ -0,0 +1,8 @@ +## Tray + +Place any PNG file in this directory to be able to use them as tray icons. +The name of the filename will be the ID to reference the image. + +Example: + +* `mypic.png` - May be referenced using `runtime.Tray.SetIcon("mypic")` diff --git a/v2/pkg/clilogger/clilogger.go b/v2/pkg/clilogger/clilogger.go new file mode 100644 index 000000000..efc202bbd --- /dev/null +++ b/v2/pkg/clilogger/clilogger.go @@ -0,0 +1,61 @@ +package clilogger + +import ( + "fmt" + "io" + "os" + + "github.com/wailsapp/wails/v2/internal/colour" +) + +// CLILogger is used by the cli +type CLILogger struct { + Writer io.Writer + mute bool +} + +// New cli logger +func New(writer io.Writer) *CLILogger { + return &CLILogger{ + Writer: writer, + } +} + +// Mute sets whether the logger should be muted +func (c *CLILogger) Mute(value bool) { + c.mute = value +} + +// Print works like Printf +func (c *CLILogger) Print(message string, args ...interface{}) { + if c.mute { + return + } + + _, err := fmt.Fprintf(c.Writer, message, args...) + if err != nil { + c.Fatal("FATAL: " + err.Error()) + } +} + +// Println works like Printf but with a line ending +func (c *CLILogger) Println(message string, args ...interface{}) { + if c.mute { + return + } + temp := fmt.Sprintf(message, args...) + _, err := fmt.Fprintln(c.Writer, temp) + if err != nil { + c.Fatal("FATAL: " + err.Error()) + } +} + +// Fatal prints the given message then aborts +func (c *CLILogger) Fatal(message string, args ...interface{}) { + temp := fmt.Sprintf(message, args...) + _, err := fmt.Fprintln(c.Writer, colour.Red("FATAL: "+temp)) + if err != nil { + println(colour.Red("FATAL: " + err.Error())) + } + os.Exit(1) +} diff --git a/v2/pkg/commands/bindings/bindings.go b/v2/pkg/commands/bindings/bindings.go new file mode 100644 index 000000000..82ce0d58f --- /dev/null +++ b/v2/pkg/commands/bindings/bindings.go @@ -0,0 +1,97 @@ +package bindings + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/shell" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" +) + +// Options for generating bindings +type Options struct { + Filename string + Tags []string + ProjectDirectory string + Compiler string + GoModTidy bool + TsPrefix string + TsSuffix string + TsOutputType string +} + +// GenerateBindings generates bindings for the Wails project in the given ProjectDirectory. +// If no project directory is given then the current working directory is used. +func GenerateBindings(options Options) (string, error) { + filename, _ := lo.Coalesce(options.Filename, "wailsbindings") + if runtime.GOOS == "windows" { + filename += ".exe" + } + + // go build -tags bindings -o bindings.exe + tempDir := os.TempDir() + filename = filepath.Join(tempDir, filename) + + workingDirectory, _ := lo.Coalesce(options.ProjectDirectory, lo.Must(os.Getwd())) + + var stdout, stderr string + var err error + + tags := append(options.Tags, "bindings") + genModuleTags := lo.Without(tags, "desktop", "production", "debug", "dev") + tagString := buildtags.Stringify(genModuleTags) + + if options.GoModTidy { + stdout, stderr, err = shell.RunCommand(workingDirectory, options.Compiler, "mod", "tidy") + if err != nil { + return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) + } + } + + envBuild := os.Environ() + envBuild = shell.SetEnv(envBuild, "GOOS", runtime.GOOS) + envBuild = shell.SetEnv(envBuild, "GOARCH", runtime.GOARCH) + // wailsbindings is executed on the build machine. + // So, use the default C compiler, not the one set for cross compiling. + envBuild = shell.RemoveEnv(envBuild, "CC") + + stdout, stderr, err = shell.RunCommandWithEnv(envBuild, workingDirectory, options.Compiler, "build", "-buildvcs=false", "-tags", tagString, "-o", filename) + if err != nil { + return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) + } + + if runtime.GOOS == "darwin" { + // Remove quarantine attribute + stdout, stderr, err = shell.RunCommand(workingDirectory, "/usr/bin/xattr", "-rc", filename) + if err != nil { + return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) + } + } + + defer func() { + // Best effort removal of temp file + _ = os.Remove(filename) + }() + + // Set environment variables accordingly + env := os.Environ() + env = shell.SetEnv(env, "tsprefix", options.TsPrefix) + env = shell.SetEnv(env, "tssuffix", options.TsSuffix) + env = shell.SetEnv(env, "tsoutputtype", options.TsOutputType) + + stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename) + if err != nil { + return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) + } + + if stderr != "" { + log.Println(colour.DarkYellow(stderr)) + } + + return stdout, nil +} diff --git a/v2/pkg/commands/bindings/bindings_test.go b/v2/pkg/commands/bindings/bindings_test.go new file mode 100644 index 000000000..53f42f2c7 --- /dev/null +++ b/v2/pkg/commands/bindings/bindings_test.go @@ -0,0 +1,128 @@ +package bindings + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/matryer/is" + "github.com/wailsapp/wails/v2/pkg/templates" +) + +const standardBindings = `// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} +` + +const obfuscatedBindings = `// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return ObfuscatedCall(0, [arg1]); +} +` + +func TestGenerateBindings(t *testing.T) { + + i := is.New(t) + + // Get the directory of this file + _, filename, _, _ := runtime.Caller(0) + workingDirectory := filepath.Dir(filename) + + projectDir := filepath.Join(workingDirectory, "test") + + _ = os.RemoveAll(projectDir) + + _, _, err := templates.Install(&templates.Options{ + ProjectName: "test", + TemplateName: "plain", + WailsVersion: "latest", + }) + if err != nil { + println(err.Error()) + t.Fail() + } + + defer func() { + _ = os.RemoveAll(projectDir) + }() + + // Make the go.mod point to local + goModPath := filepath.Join(projectDir, "go.mod") + goMod, err := os.ReadFile(goModPath) + i.NoErr(err) + pathToRepository := filepath.Join(workingDirectory, "..", "..", "..") + absPathToRepo, _ := filepath.Abs(pathToRepository) + goModString := string(goMod) + goModSplit := strings.Split(goModString, "=>") + goModSplit[1] = absPathToRepo + goModString = strings.Join(goModSplit, "=> ") + goMod = []byte(strings.ReplaceAll(goModString, "// replace", "replace")) + // Write file back + err = os.WriteFile(goModPath, goMod, 0755) + i.NoErr(err) + + tests := []struct { + name string + options Options + stdout string + expectedBindings string + wantErr bool + }{ + { + name: "should generate standard bindings with no user tags", + options: Options{ + ProjectDirectory: projectDir, + Compiler: "go", + GoModTidy: true, + }, + expectedBindings: standardBindings, + stdout: "", + wantErr: false, + }, + { + name: "should generate bindings when given tags", + options: Options{ + ProjectDirectory: projectDir, + Compiler: "go", + Tags: []string{"test"}, + GoModTidy: true, + }, + expectedBindings: standardBindings, + stdout: "", + wantErr: false, + }, + { + name: "should generate obfuscated bindings", + options: Options{ + ProjectDirectory: projectDir, + Compiler: "go", + Tags: []string{"obfuscated"}, + GoModTidy: true, + }, + expectedBindings: obfuscatedBindings, + stdout: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout, err := GenerateBindings(tt.options) + i.True((err != nil) == tt.wantErr) + i.Equal(stdout, tt.stdout) + // Read bindings + bindingsFile := filepath.Join(projectDir, "frontend", "wailsjs", "go", "main", "App.js") + bindings, err := os.ReadFile(bindingsFile) + i.NoErr(err) + i.Equal(string(bindings), tt.expectedBindings) + }) + } +} diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go new file mode 100644 index 000000000..239932ce8 --- /dev/null +++ b/v2/pkg/commands/build/base.go @@ -0,0 +1,612 @@ +package build + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/pterm/pterm" + + "github.com/wailsapp/wails/v2/internal/system" + + "github.com/leaanthony/gosod" + "github.com/wailsapp/wails/v2/internal/frontend/runtime/wrapper" + + "github.com/pkg/errors" + + "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/internal/shell" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +const ( + VERBOSE int = 2 +) + +// BaseBuilder is the common builder struct +type BaseBuilder struct { + filesToDelete slicer.StringSlicer + projectData *project.Project + options *Options +} + +// NewBaseBuilder creates a new BaseBuilder +func NewBaseBuilder(options *Options) *BaseBuilder { + result := &BaseBuilder{ + options: options, + } + return result +} + +// SetProjectData sets the project data for this builder +func (b *BaseBuilder) SetProjectData(projectData *project.Project) { + b.projectData = projectData +} + +func (b *BaseBuilder) addFileToDelete(filename string) { + if !b.options.KeepAssets { + b.filesToDelete.Add(filename) + } +} + +func (b *BaseBuilder) fileExists(path string) bool { + // if file doesn't exist, ignore + _, err := os.Stat(path) + if err != nil { + return !os.IsNotExist(err) + } + return true +} + +func (b *BaseBuilder) convertFileToIntegerString(filename string) (string, error) { + rawData, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return b.convertByteSliceToIntegerString(rawData), nil +} + +func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string { + // Create string builder + var result strings.Builder + + if len(data) > 0 { + + // Loop over all but 1 bytes + for i := 0; i < len(data)-1; i++ { + result.WriteString(fmt.Sprintf("%v,", data[i])) + } + + result.WriteString(strconv.FormatUint(uint64(data[len(data)-1]), 10)) + } + + return result.String() +} + +// CleanUp does post-build housekeeping +func (b *BaseBuilder) CleanUp() { + // Delete all the files + b.filesToDelete.Each(func(filename string) { + // if file doesn't exist, ignore + if !b.fileExists(filename) { + return + } + + // Delete file. We ignore errors because these files will be overwritten + // by the next build anyway. + _ = os.Remove(filename) + }) +} + +func commandPrettifier(args []string) string { + // If we have a single argument, just return it + if len(args) == 1 { + return args[0] + } + // If an argument contains a space, quote it + for i, arg := range args { + if strings.Contains(arg, " ") { + args[i] = fmt.Sprintf("\"%s\"", arg) + } + } + return strings.Join(args, " ") +} + +func (b *BaseBuilder) OutputFilename(options *Options) string { + outputFile := options.OutputFile + if outputFile == "" { + target := strings.TrimSuffix(b.projectData.OutputFilename, ".exe") + if b.projectData.OutputType != "desktop" { + target += "-" + b.projectData.OutputType + } + // If we aren't using the standard compiler, add it to the filename + if options.Compiler != "go" { + // Parse the `go version` output. EG: `go version go1.16 windows/amd64` + stdout, _, err := shell.RunCommand(".", options.Compiler, "version") + if err != nil { + return "" + } + versionSplit := strings.Split(stdout, " ") + if len(versionSplit) == 4 { + target += "-" + versionSplit[2] + } + } + switch b.options.Platform { + case "windows": + outputFile = target + ".exe" + case "darwin", "linux": + if b.options.Arch == "" { + b.options.Arch = runtime.GOARCH + } + outputFile = fmt.Sprintf("%s-%s-%s", target, b.options.Platform, b.options.Arch) + } + + } + return outputFile +} + +// CompileProject compiles the project +func (b *BaseBuilder) CompileProject(options *Options) error { + // Check if the runtime wrapper exists + err := generateRuntimeWrapper(options) + if err != nil { + return err + } + + verbose := options.Verbosity == VERBOSE + // Run go mod tidy first + if !options.SkipModTidy { + cmd := exec.Command(options.Compiler, "mod", "tidy") + cmd.Stderr = os.Stderr + if verbose { + println("") + cmd.Stdout = os.Stdout + } + err = cmd.Run() + if err != nil { + return err + } + } + + commands := slicer.String() + + compiler := options.Compiler + if options.Obfuscated { + if !shell.CommandExists("garble") { + return fmt.Errorf("the 'garble' command was not found. Please install it with `go install mvdan.cc/garble@latest`") + } else { + compiler = "garble" + if options.GarbleArgs != "" { + commands.AddSlice(strings.Split(options.GarbleArgs, " ")) + } + options.UserTags = append(options.UserTags, "obfuscated") + } + } + + // Default go build command + commands.Add("build") + + commands.Add("-buildvcs=false") + + // Add better debugging flags + if options.Mode == Dev || options.Mode == Debug { + commands.Add("-gcflags") + commands.Add("all=-N -l") + } + + if options.ForceBuild { + commands.Add("-a") + } + + if options.TrimPath { + commands.Add("-trimpath") + } + + if options.RaceDetector { + commands.Add("-race") + } + + var tags slicer.StringSlicer + tags.Add(options.OutputType) + tags.AddSlice(options.UserTags) + + // Add webview2 strategy if we have it + if options.WebView2Strategy != "" { + tags.Add(options.WebView2Strategy) + } + + if options.Mode == Production || options.Mode == Debug { + tags.Add("production") + } + // This mode allows you to debug a production build (not dev build) + if options.Mode == Debug { + tags.Add("debug") + } + + // This options allows you to enable devtools in production build (not dev build as it's always enabled there) + if options.Devtools { + tags.Add("devtools") + } + + if options.Obfuscated { + tags.Add("obfuscated") + } + + tags.Deduplicate() + + // Add the output type build tag + commands.Add("-tags") + commands.Add(tags.Join(",")) + + // LDFlags + ldflags := slicer.String() + if options.LDFlags != "" { + ldflags.Add(options.LDFlags) + } + + if options.Mode == Production { + ldflags.Add("-w", "-s") + if options.Platform == "windows" && !options.WindowsConsole { + ldflags.Add("-H windowsgui") + } + } + + ldflags.Deduplicate() + + if ldflags.Length() > 0 { + commands.Add("-ldflags") + commands.Add(ldflags.Join(" ")) + } + + // Get application build directory + appDir := options.BinDirectory + if options.CleanBinDirectory { + err = cleanBinDirectory(options) + if err != nil { + return err + } + } + + // Set up output filename + outputFile := b.OutputFilename(options) + compiledBinary := filepath.Join(appDir, outputFile) + commands.Add("-o") + commands.Add(compiledBinary) + + options.CompiledBinary = compiledBinary + + // Build the application + cmd := exec.Command(compiler, commands.AsSlice()...) + cmd.Stderr = os.Stderr + if verbose { + pterm.Info.Println("Build command:", compiler, commandPrettifier(commands.AsSlice())) + cmd.Stdout = os.Stdout + } + // Set the directory + cmd.Dir = b.projectData.Path + + // Add CGO flags + // TODO: Remove this as we don't generate headers any more + // We use the project/build dir as a temporary place for our generated c headers + buildBaseDir, err := fs.RelativeToCwd("build") + if err != nil { + return err + } + + cmd.Env = os.Environ() // inherit env + + if options.Platform != "windows" { + // Use shell.UpsertEnv so we don't overwrite user's CGO_CFLAGS + cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string { + if options.Platform == "darwin" { + if v != "" { + v += " " + } + if !strings.Contains(v, "-mmacosx-version-min") { + v += "-mmacosx-version-min=10.13" + } + } + return v + }) + // Use shell.UpsertEnv so we don't overwrite user's CGO_CXXFLAGS + cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CXXFLAGS", func(v string) string { + if v != "" { + v += " " + } + v += "-I" + buildBaseDir + return v + }) + + cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string { + return "1" + }) + if options.Platform == "darwin" { + // Determine version so we can link to newer frameworks + // Why doesn't CGO have this option?!?! + info, err := system.GetInfo() + if err != nil { + return err + } + versionSplit := strings.Split(info.OS.Version, ".") + majorVersion, err := strconv.Atoi(versionSplit[0]) + if err != nil { + return err + } + addUTIFramework := majorVersion >= 11 + // Set the minimum Mac SDK to 10.13 + cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_LDFLAGS", func(v string) string { + if v != "" { + v += " " + } + if addUTIFramework { + v += "-framework UniformTypeIdentifiers " + } + if !strings.Contains(v, "-mmacosx-version-min") { + v += "-mmacosx-version-min=10.13" + } + + return v + }) + } + } + + cmd.Env = shell.UpsertEnv(cmd.Env, "GOOS", func(v string) string { + return options.Platform + }) + + cmd.Env = shell.UpsertEnv(cmd.Env, "GOARCH", func(v string) string { + return options.Arch + }) + + if verbose { + printBulletPoint("Environment:", strings.Join(cmd.Env, " ")) + } + + // Run command + err = cmd.Run() + cmd.Stderr = os.Stderr + + // Format error if we have one + if err != nil { + if options.Platform == "darwin" { + output, _ := cmd.CombinedOutput() + stdErr := string(output) + if strings.Contains(err.Error(), "ld: framework not found UniformTypeIdentifiers") || + strings.Contains(stdErr, "ld: framework not found UniformTypeIdentifiers") { + pterm.Warning.Println(` +NOTE: It would appear that you do not have the latest Xcode cli tools installed. +Please reinstall by doing the following: + 1. Remove the current installation located at "xcode-select -p", EG: sudo rm -rf /Library/Developer/CommandLineTools + 2. Install latest Xcode tools: xcode-select --install`) + } + } + return err + } + + if !options.Compress { + return nil + } + + printBulletPoint("Compressing application: ") + + // Do we have upx installed? + if !shell.CommandExists("upx") { + pterm.Warning.Println("Warning: Cannot compress binary: upx not found") + return nil + } + + args := []string{"--best", "--no-color", "--no-progress", options.CompiledBinary} + + if options.CompressFlags != "" { + args = strings.Split(options.CompressFlags, " ") + args = append(args, options.CompiledBinary) + } + + if verbose { + pterm.Info.Println("upx", strings.Join(args, " ")) + } + + output, err := exec.Command("upx", args...).Output() + if err != nil { + return errors.Wrap(err, "Error during compression:") + } + pterm.Println("Done.") + if verbose { + pterm.Info.Println(string(output)) + } + + return nil +} + +func generateRuntimeWrapper(options *Options) error { + if options.WailsJSDir == "" { + cwd, err := os.Getwd() + if err != nil { + return err + } + options.WailsJSDir = filepath.Join(cwd, "frontend") + } + wrapperDir := filepath.Join(options.WailsJSDir, "wailsjs", "runtime") + _ = os.RemoveAll(wrapperDir) + extractor := gosod.New(wrapper.RuntimeWrapper) + err := extractor.Extract(wrapperDir, nil) + if err != nil { + return err + } + + return nil +} + +// NpmInstall runs "npm install" in the given directory +func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error { + return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose) +} + +// NpmInstallUsingCommand runs the given install command in the specified npm project directory +func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error { + packageJSON := filepath.Join(sourceDir, "package.json") + + // Check package.json exists + if !fs.FileExists(packageJSON) { + // No package.json, no install + return nil + } + + install := false + + // Get the MD5 sum of package.json + packageJSONMD5 := fs.MustMD5File(packageJSON) + + // Check whether we need to npm install + packageChecksumFile := filepath.Join(sourceDir, "package.json.md5") + if fs.FileExists(packageChecksumFile) { + // Compare checksums + storedChecksum := fs.MustLoadString(packageChecksumFile) + if storedChecksum != packageJSONMD5 { + fs.MustWriteString(packageChecksumFile, packageJSONMD5) + install = true + } + } else { + install = true + fs.MustWriteString(packageChecksumFile, packageJSONMD5) + } + + // Install if node_modules doesn't exist + nodeModulesDir := filepath.Join(sourceDir, "node_modules") + if !fs.DirExists(nodeModulesDir) { + install = true + } + + // check if forced install + if b.options.ForceBuild { + install = true + } + + // Shortcut installation + if !install { + if verbose { + pterm.Println("Skipping npm install") + } + return nil + } + + // Split up the InstallCommand and execute it + cmd := strings.Split(installCommand, " ") + stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...) + if verbose || err != nil { + for _, l := range strings.Split(stdout, "\n") { + pterm.Printf(" %s\n", l) + } + for _, l := range strings.Split(stderr, "\n") { + pterm.Printf(" %s\n", l) + } + } + + return err +} + +// NpmRun executes the npm target in the provided directory +func (b *BaseBuilder) NpmRun(projectDir, buildTarget string, verbose bool) error { + stdout, stderr, err := shell.RunCommand(projectDir, "npm", "run", buildTarget) + if verbose || err != nil { + for _, l := range strings.Split(stdout, "\n") { + pterm.Printf(" %s\n", l) + } + for _, l := range strings.Split(stderr, "\n") { + pterm.Printf(" %s\n", l) + } + } + return err +} + +// NpmRunWithEnvironment executes the npm target in the provided directory, with the given environment variables +func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verbose bool, envvars []string) error { + cmd := shell.CreateCommand(projectDir, "npm", "run", buildTarget) + cmd.Env = append(os.Environ(), envvars...) + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + err := cmd.Run() + if verbose || err != nil { + for _, l := range strings.Split(stdo.String(), "\n") { + pterm.Printf(" %s\n", l) + } + for _, l := range strings.Split(stde.String(), "\n") { + pterm.Printf(" %s\n", l) + } + } + return err +} + +// BuildFrontend executes the `npm build` command for the frontend directory +func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { + verbose := b.options.Verbosity == VERBOSE + + frontendDir := b.projectData.GetFrontendDir() + if !fs.DirExists(frontendDir) { + return fmt.Errorf("frontend directory '%s' does not exist", frontendDir) + } + + // Check there is an 'InstallCommand' provided in wails.json + installCommand := b.projectData.InstallCommand + if b.projectData.OutputType == "dev" { + installCommand = b.projectData.GetDevInstallerCommand() + } + if installCommand == "" { + // No - don't install + printBulletPoint("No Install command. Skipping.") + pterm.Println("") + } else { + // Do install if needed + printBulletPoint("Installing frontend dependencies: ") + if verbose { + pterm.Println("") + pterm.Info.Println("Install command: '" + installCommand + "'") + } + if err := b.NpmInstallUsingCommand(frontendDir, installCommand, verbose); err != nil { + return err + } + outputLogger.Println("Done.") + } + + // Check if there is a build command + buildCommand := b.projectData.BuildCommand + if b.projectData.OutputType == "dev" { + buildCommand = b.projectData.GetDevBuildCommand() + } + if buildCommand == "" { + printBulletPoint("No Build command. Skipping.") + pterm.Println("") + // No - ignore + return nil + } + + printBulletPoint("Compiling frontend: ") + cmd := strings.Split(buildCommand, " ") + if verbose { + pterm.Println("") + pterm.Info.Println("Build command: '" + buildCommand + "'") + } + stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...) + if verbose || err != nil { + for _, l := range strings.Split(stdout, "\n") { + pterm.Printf(" %s\n", l) + } + for _, l := range strings.Split(stderr, "\n") { + pterm.Printf(" %s\n", l) + } + } + if err != nil { + return err + } + + pterm.Println("Done.") + return nil +} diff --git a/v2/pkg/commands/build/base_test.go b/v2/pkg/commands/build/base_test.go new file mode 100644 index 000000000..3b48b24b6 --- /dev/null +++ b/v2/pkg/commands/build/base_test.go @@ -0,0 +1,34 @@ +package build + +import "testing" + +func Test_commandPrettifier(t *testing.T) { + tests := []struct { + name string + input []string + want string + }{ + { + name: "empty", + input: []string{}, + want: "", + }, + { + name: "one arg", + input: []string{"one"}, + want: "one", + }, + { + name: "args where one has spaces", + input: []string{"one", "two three"}, + want: `one "two three"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := commandPrettifier(tt.input); got != tt.want { + t.Errorf("commandPrettifier() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go new file mode 100644 index 000000000..7263f63ae --- /dev/null +++ b/v2/pkg/commands/build/build.go @@ -0,0 +1,450 @@ +package build + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/google/shlex" + "github.com/pterm/pterm" + "github.com/samber/lo" + + "github.com/wailsapp/wails/v2/internal/staticanalysis" + "github.com/wailsapp/wails/v2/pkg/commands/bindings" + + "github.com/wailsapp/wails/v2/internal/fs" + + "github.com/wailsapp/wails/v2/internal/shell" + + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +// Mode is the type used to indicate the build modes +type Mode int + +const ( + // Dev mode + Dev Mode = iota + // Production mode + Production + // Debug build + Debug +) + +// Options contains all the build options as well as the project data +type Options struct { + LDFlags string // Optional flags to pass to linker + UserTags []string // Tags to pass to the Go compiler + Logger *clilogger.CLILogger // All output to the logger + OutputType string // EG: desktop, server.... + Mode Mode // release or dev + Devtools bool // Enable devtools in production + ProjectData *project.Project // The project data + Pack bool // Create a package for the app after building + Platform string // The platform to build for + Arch string // The architecture to build for + Compiler string // The compiler command to use + SkipModTidy bool // Skip mod tidy before compile + IgnoreFrontend bool // Indicates if the frontend does not need building + IgnoreApplication bool // Indicates if the application does not need building + OutputFile string // Override the output filename + BinDirectory string // Directory to use to write the built applications + CleanBinDirectory bool // Indicates if the bin output directory should be cleaned before building + CompiledBinary string // Fully qualified path to the compiled binary + KeepAssets bool // Keep the generated assets/files + Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose) + Compress bool // Compress the final binary + CompressFlags string // Flags to pass to UPX + WebView2Strategy string // WebView2 installer strategy + RunDelve bool // Indicates if we should run delve after the build + WailsJSDir string // Directory to generate the wailsjs module + ForceBuild bool // Force + BundleName string // Bundlename for Mac + TrimPath bool // Use Go's trimpath compiler flag + RaceDetector bool // Build with Go's race detector + WindowsConsole bool // Indicates that the windows console should be kept + Obfuscated bool // Indicates that bound methods should be obfuscated + GarbleArgs string // The arguments for Garble + SkipBindings bool // Skip binding generation + SkipEmbedCreate bool // Skip creation of embed files +} + +// Build the project! +func Build(options *Options) (string, error) { + // Extract logger + outputLogger := options.Logger + + // Get working directory + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + // wails js dir + options.WailsJSDir = options.ProjectData.GetWailsJSDir() + + // Set build directory + options.BinDirectory = filepath.Join(options.ProjectData.GetBuildDir(), "bin") + + // Save the project type + options.ProjectData.OutputType = options.OutputType + + // Create builder + var builder Builder + + switch options.OutputType { + case "desktop": + builder = newDesktopBuilder(options) + case "dev": + builder = newDesktopBuilder(options) + default: + return "", fmt.Errorf("cannot build assets for output type %s", options.ProjectData.OutputType) + } + + // Set up our clean up method + defer builder.CleanUp() + + // Initialise Builder + builder.SetProjectData(options.ProjectData) + + hookArgs := map[string]string{ + "${platform}": options.Platform + "/" + options.Arch, + } + + for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} { + if err := execPreBuildHook(outputLogger, options, hook, hookArgs); err != nil { + return "", err + } + } + + // Create embed directories if they don't exist + if !options.SkipEmbedCreate { + if err := CreateEmbedDirectories(cwd, options); err != nil { + return "", err + } + } + + // Generate bindings + if !options.SkipBindings { + err = GenerateBindings(options) + if err != nil { + return "", err + } + } + + if !options.IgnoreFrontend { + err = builder.BuildFrontend(outputLogger) + if err != nil { + return "", err + } + } + + compileBinary := "" + if !options.IgnoreApplication { + compileBinary, err = execBuildApplication(builder, options) + if err != nil { + return "", err + } + + hookArgs["${bin}"] = compileBinary + for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} { + if err := execPostBuildHook(outputLogger, options, hook, hookArgs); err != nil { + return "", err + } + } + + } + return compileBinary, nil +} + +func CreateEmbedDirectories(cwd string, buildOptions *Options) error { + path := cwd + if buildOptions.ProjectData != nil { + path = buildOptions.ProjectData.Path + } + embedDetails, err := staticanalysis.GetEmbedDetails(path) + if err != nil { + return err + } + + for _, embedDetail := range embedDetails { + fullPath := embedDetail.GetFullPath() + // assumes path is directory only if it has no extension + if filepath.Ext(fullPath) == "" { + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + err := os.MkdirAll(fullPath, 0o755) + if err != nil { + return err + } + f, err := os.Create(filepath.Join(fullPath, "gitkeep")) + if err != nil { + return err + } + _ = f.Close() + } + } + } + + return nil +} + +func fatal(message string) { + printer := pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalPrefixStyle, + Text: " FATAL ", + }, + } + printer.Println(message) + os.Exit(1) +} + +func printBulletPoint(text string, args ...any) { + item := pterm.BulletListItem{ + Level: 2, + Text: text, + } + t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender() + if err != nil { + fatal(err.Error()) + } + t = strings.Trim(t, "\n\r") + pterm.Printf(t, args...) +} + +func GenerateBindings(buildOptions *Options) error { + obfuscated := buildOptions.Obfuscated + if obfuscated { + printBulletPoint("Generating obfuscated bindings: ") + buildOptions.UserTags = append(buildOptions.UserTags, "obfuscated") + } else { + printBulletPoint("Generating bindings: ") + } + + if buildOptions.ProjectData.Bindings.TsGeneration.OutputType == "" { + buildOptions.ProjectData.Bindings.TsGeneration.OutputType = "classes" + } + + // Generate Bindings + output, err := bindings.GenerateBindings(bindings.Options{ + Compiler: buildOptions.Compiler, + Tags: buildOptions.UserTags, + GoModTidy: !buildOptions.SkipModTidy, + TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix, + TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix, + TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType, + }) + if err != nil { + return err + } + + if buildOptions.Verbosity == VERBOSE { + pterm.Info.Println(output) + } + + pterm.Println("Done.") + + return nil +} + +func execBuildApplication(builder Builder, options *Options) (string, error) { + // If we are building for windows, we will need to generate the asset bundle before + // compilation. This will be a .syso file in the project root + if options.Pack && options.Platform == "windows" { + printBulletPoint("Generating application assets: ") + err := packageApplicationForWindows(options) + if err != nil { + return "", err + } + pterm.Println("Done.") + + // When we finish, we will want to remove the syso file + defer func() { + err := os.Remove(filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso")) + if err != nil { + fatal(err.Error()) + } + }() + } + + // Compile the application + printBulletPoint("Compiling application: ") + + if options.Platform == "darwin" && options.Arch == "universal" { + outputFile := builder.OutputFilename(options) + amd64Filename := outputFile + "-amd64" + arm64Filename := outputFile + "-arm64" + + // Build amd64 first + options.Arch = "amd64" + options.OutputFile = amd64Filename + options.CleanBinDirectory = false + if options.Verbosity == VERBOSE { + pterm.Println("Building AMD64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) + } + err := builder.CompileProject(options) + if err != nil { + return "", err + } + // Build arm64 + options.Arch = "arm64" + options.OutputFile = arm64Filename + options.CleanBinDirectory = false + if options.Verbosity == VERBOSE { + pterm.Println("Building ARM64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) + } + err = builder.CompileProject(options) + + if err != nil { + return "", err + } + // Run lipo + if options.Verbosity == VERBOSE { + pterm.Println(fmt.Sprintf("Running lipo: lipo -create -output %s %s %s", outputFile, amd64Filename, arm64Filename)) + } + _, stderr, err := shell.RunCommand(options.BinDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename) + if err != nil { + return "", fmt.Errorf("%s - %s", err.Error(), stderr) + } + // Remove temp binaries + err = fs.DeleteFile(filepath.Join(options.BinDirectory, amd64Filename)) + if err != nil { + return "", err + } + err = fs.DeleteFile(filepath.Join(options.BinDirectory, arm64Filename)) + if err != nil { + return "", err + } + options.ProjectData.OutputFilename = outputFile + options.CompiledBinary = filepath.Join(options.BinDirectory, outputFile) + } else { + err := builder.CompileProject(options) + if err != nil { + return "", err + } + } + + if runtime.GOOS == "darwin" { + // Remove quarantine attribute + if _, err := os.Stat(options.CompiledBinary); os.IsNotExist(err) { + return "", fmt.Errorf("compiled binary does not exist at path: %s", options.CompiledBinary) + } + stdout, stderr, err := shell.RunCommand(options.BinDirectory, "/usr/bin/xattr", "-rc", options.CompiledBinary) + if err != nil { + return "", fmt.Errorf("%s - %s", err.Error(), stderr) + } + if options.Verbosity == VERBOSE && stdout != "" { + pterm.Info.Println(stdout) + } + } + + pterm.Println("Done.") + + // Do we need to pack the app for non-windows? + if options.Pack && options.Platform != "windows" { + + printBulletPoint("Packaging application: ") + + // TODO: Allow cross platform build + err := packageProject(options, runtime.GOOS) + if err != nil { + return "", err + } + pterm.Println("Done.") + } + + if options.Platform == "windows" { + const nativeWebView2Loader = "native_webview2loader" + + tags := options.UserTags + if lo.Contains(tags, nativeWebView2Loader) { + message := "You are using the legacy native WebView2Loader. This loader will be deprecated in the near future. Please report any bugs related to the new loader: https://github.com/wailsapp/wails/issues/2004" + pterm.Warning.Println(message) + } else { + tags = append(tags, nativeWebView2Loader) + message := fmt.Sprintf("Wails is now using the new Go WebView2Loader. If you encounter any issues with it, please report them to https://github.com/wailsapp/wails/issues/2004. You could also use the old legacy loader with `-tags %s`, but keep in mind this will be deprecated in the near future.", strings.Join(tags, ",")) + pterm.Info.Println(message) + } + } + + if options.Platform == "darwin" && (options.Mode == Debug || options.Devtools) { + pterm.Warning.Println("This darwin build contains the use of private APIs. This will not pass Apple's AppStore approval process. Please use it only as a test build for testing and debug purposes.") + } + + return options.CompiledBinary, nil +} + +func execPreBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error { + preBuildHook := options.ProjectData.PreBuildHooks[hookIdentifier] + if preBuildHook == "" { + return nil + } + + return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, preBuildHook, "pre") +} + +func execPostBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error { + postBuildHook := options.ProjectData.PostBuildHooks[hookIdentifier] + if postBuildHook == "" { + return nil + } + + return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, postBuildHook, "post") +} + +func executeBuildHook(_ *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string, buildHook string, hookName string) error { + if !options.ProjectData.RunNonNativeBuildHooks { + if hookIdentifier == "" { + // That's the global hook + } else { + platformOfHook := strings.Split(hookIdentifier, "/")[0] + if platformOfHook == "*" { + // That's OK, we don't have a specific platform of the hook + } else if platformOfHook == runtime.GOOS { + // The hook is for host platform + } else { + // Skip a hook which is not native + printBulletPoint(fmt.Sprintf("Non native build hook '%s': Skipping.", hookIdentifier)) + return nil + } + } + } + + printBulletPoint("Executing %s build hook '%s': ", hookName, hookIdentifier) + args, err := shlex.Split(buildHook) + if err != nil { + return fmt.Errorf("could not parse %s build hook command: %w", hookName, err) + } + for i, arg := range args { + newArg := argReplacements[arg] + if newArg == "" { + continue + } + args[i] = newArg + } + + if options.Verbosity == VERBOSE { + pterm.Info.Println(strings.Join(args, " ")) + } + + if !fs.DirExists(options.BinDirectory) { + if err := fs.MkDirs(options.BinDirectory); err != nil { + return fmt.Errorf("could not create target directory: %s", err.Error()) + } + } + + stdout, stderr, err := shell.RunCommand(options.BinDirectory, args[0], args[1:]...) + if options.Verbosity == VERBOSE { + pterm.Info.Println(stdout) + } + if err != nil { + return fmt.Errorf("%s - %s", err.Error(), stderr) + } + pterm.Println("Done.") + + return nil +} diff --git a/v2/pkg/commands/build/builder.go b/v2/pkg/commands/build/builder.go new file mode 100644 index 000000000..6a220c530 --- /dev/null +++ b/v2/pkg/commands/build/builder.go @@ -0,0 +1,15 @@ +package build + +import ( + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +// Builder defines a builder that can build Wails applications +type Builder interface { + SetProjectData(projectData *project.Project) + BuildFrontend(logger *clilogger.CLILogger) error + CompileProject(options *Options) error + OutputFilename(options *Options) string + CleanUp() +} diff --git a/v2/pkg/commands/build/desktop.go b/v2/pkg/commands/build/desktop.go new file mode 100644 index 000000000..c54eb3035 --- /dev/null +++ b/v2/pkg/commands/build/desktop.go @@ -0,0 +1,12 @@ +package build + +// DesktopBuilder builds applications for the desktop +type DesktopBuilder struct { + *BaseBuilder +} + +func newDesktopBuilder(options *Options) *DesktopBuilder { + return &DesktopBuilder{ + BaseBuilder: NewBaseBuilder(options), + } +} diff --git a/v2/pkg/commands/build/internal/packager/darwin/Info.plist b/v2/pkg/commands/build/internal/packager/darwin/Info.plist new file mode 100644 index 000000000..9736913ae --- /dev/null +++ b/v2/pkg/commands/build/internal/packager/darwin/Info.plist @@ -0,0 +1,14 @@ + + + CFBundlePackageTypeAPPL + CFBundleName{{.Title}} + CFBundleExecutable{{.Title}} + CFBundleIdentifiercom.wails.{{.Title}} + CFBundleVersion1.0.0 + CFBundleGetInfoStringBuilt using Wails (https://wails.io) + CFBundleShortVersionString1.0.0 + CFBundleIconFileiconfile + LSMinimumSystemVersion10.13.0 + NSHighResolutionCapabletrue + NSHumanReadableCopyrightCopyright......... + \ No newline at end of file diff --git a/v2/pkg/commands/build/internal/packager/icon1024.png b/v2/pkg/commands/build/internal/packager/icon1024.png new file mode 100644 index 000000000..a3ad26ce7 Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon1024.png differ diff --git a/v2/pkg/commands/build/internal/packager/icon128.png b/v2/pkg/commands/build/internal/packager/icon128.png new file mode 100644 index 000000000..ea74d31b5 Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon128.png differ diff --git a/v2/pkg/commands/build/internal/packager/icon256.png b/v2/pkg/commands/build/internal/packager/icon256.png new file mode 100644 index 000000000..d7031f3a6 Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon256.png differ diff --git a/v2/pkg/commands/build/internal/packager/icon32.png b/v2/pkg/commands/build/internal/packager/icon32.png new file mode 100644 index 000000000..231136baa Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon32.png differ diff --git a/v2/pkg/commands/build/internal/packager/icon512.png b/v2/pkg/commands/build/internal/packager/icon512.png new file mode 100644 index 000000000..53c612c7b Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon512.png differ diff --git a/v2/pkg/commands/build/internal/packager/icon64.png b/v2/pkg/commands/build/internal/packager/icon64.png new file mode 100644 index 000000000..a2b304154 Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon64.png differ diff --git a/v2/pkg/commands/build/internal/packager/linux/app.desktop b/v2/pkg/commands/build/internal/packager/linux/app.desktop new file mode 100644 index 000000000..59f0456d2 --- /dev/null +++ b/v2/pkg/commands/build/internal/packager/linux/app.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Exec={{.Name}} +Name={{.Name}} +Icon={{.Name}} +Categories=Utility; \ No newline at end of file diff --git a/v2/pkg/commands/build/nsis_installer.go b/v2/pkg/commands/build/nsis_installer.go new file mode 100644 index 000000000..820df2d1d --- /dev/null +++ b/v2/pkg/commands/build/nsis_installer.go @@ -0,0 +1,119 @@ +package build + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/shell" + "github.com/wailsapp/wails/v2/internal/webview2runtime" + "github.com/wailsapp/wails/v2/pkg/buildassets" +) + +const ( + nsisTypeSingle = "single" + nsisTypeMultiple = "multiple" + + nsisFolder = "windows/installer" + nsisProjectFile = "project.nsi" + nsisToolsFile = "wails_tools.nsh" + nsisWebView2SetupFile = "tmp/MicrosoftEdgeWebview2Setup.exe" +) + +func GenerateNSISInstaller(options *Options, amd64Binary string, arm64Binary string) error { + outputLogger := options.Logger + outputLogger.Println("Creating NSIS installer\n------------------------------") + + // Ensure the file exists, if not the template will be written. + projectFile := path.Join(nsisFolder, nsisProjectFile) + if _, err := buildassets.ReadFile(options.ProjectData, projectFile); err != nil { + return fmt.Errorf("Unable to generate NSIS installer project template: %w", err) + } + + // Write the resolved nsis tools + toolsFile := path.Join(nsisFolder, nsisToolsFile) + if _, err := buildassets.ReadOriginalFileWithProjectDataAndSave(options.ProjectData, toolsFile); err != nil { + return fmt.Errorf("Unable to generate NSIS tools file: %w", err) + } + + // Write the WebView2 SetupFile + webviewSetup := buildassets.GetLocalPath(options.ProjectData, path.Join(nsisFolder, nsisWebView2SetupFile)) + if dir := filepath.Dir(webviewSetup); !fs.DirExists(dir) { + if err := fs.MkDirs(dir, 0o755); err != nil { + return err + } + } + + if err := webview2runtime.WriteInstallerToFile(webviewSetup); err != nil { + return fmt.Errorf("Unable to write WebView2 Bootstrapper Setup: %w", err) + } + + if !shell.CommandExists("makensis") { + outputLogger.Println("Warning: Cannot create installer: makensis not found") + return nil + } + + nsisType := options.ProjectData.NSISType + if nsisType == nsisTypeSingle && (amd64Binary == "" || arm64Binary == "") { + nsisType = "" + } + + switch nsisType { + case "": + fallthrough + case nsisTypeMultiple: + if amd64Binary != "" { + if err := makeNSIS(options, "amd64", amd64Binary, ""); err != nil { + return err + } + } + + if arm64Binary != "" { + if err := makeNSIS(options, "arm64", "", arm64Binary); err != nil { + return err + } + } + + case nsisTypeSingle: + if err := makeNSIS(options, "single", amd64Binary, arm64Binary); err != nil { + return err + } + default: + return fmt.Errorf("Unsupported nsisType: %s", nsisType) + } + + return nil +} + +func makeNSIS(options *Options, installerKind string, amd64Binary string, arm64Binary string) error { + verbose := options.Verbosity == VERBOSE + outputLogger := options.Logger + + outputLogger.Print(" - Building '%s' installer: ", installerKind) + args := []string{} + if amd64Binary != "" { + args = append(args, "-DARG_WAILS_AMD64_BINARY="+amd64Binary) + } + if arm64Binary != "" { + args = append(args, "-DARG_WAILS_ARM64_BINARY="+arm64Binary) + } + args = append(args, nsisProjectFile) + + if verbose { + outputLogger.Println("makensis %s", strings.Join(args, " ")) + } + + installerDir := buildassets.GetLocalPath(options.ProjectData, nsisFolder) + stdOut, stdErr, err := shell.RunCommand(installerDir, "makensis", args...) + if err != nil || verbose { + outputLogger.Println(stdOut) + outputLogger.Println(stdErr) + } + if err != nil { + return fmt.Errorf("Error during creation of the installer: %w", err) + } + outputLogger.Println("Done.") + return nil +} diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go new file mode 100644 index 000000000..d406256f9 --- /dev/null +++ b/v2/pkg/commands/build/packager.go @@ -0,0 +1,301 @@ +package build + +import ( + "bytes" + "fmt" + "image" + "os" + "path/filepath" + "strings" + + "github.com/leaanthony/winicon" + "github.com/tc-hib/winres" + "github.com/tc-hib/winres/version" + "github.com/wailsapp/wails/v2/internal/project" + + "github.com/jackmordaunt/icns" + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/pkg/buildassets" + + "github.com/wailsapp/wails/v2/internal/fs" +) + +// PackageProject packages the application +func packageProject(options *Options, platform string) error { + var err error + switch platform { + case "darwin": + err = packageApplicationForDarwin(options) + case "windows": + err = packageApplicationForWindows(options) + case "linux": + err = packageApplicationForLinux(options) + default: + err = fmt.Errorf("packing not supported for %s yet", platform) + } + + if err != nil { + return err + } + + return nil +} + +// cleanBinDirectory will remove an existing bin directory and recreate it +func cleanBinDirectory(options *Options) error { + buildDirectory := options.BinDirectory + + // Clear out old builds + if fs.DirExists(buildDirectory) { + err := os.RemoveAll(buildDirectory) + if err != nil { + return err + } + } + + // Create clean directory + err := os.MkdirAll(buildDirectory, 0o700) + if err != nil { + return err + } + + return nil +} + +func packageApplicationForDarwin(options *Options) error { + var err error + + // Create directory structure + bundlename := options.BundleName + if bundlename == "" { + bundlename = options.ProjectData.Name + ".app" + } + + contentsDirectory := filepath.Join(options.BinDirectory, bundlename, "/Contents") + exeDir := filepath.Join(contentsDirectory, "/MacOS") + err = fs.MkDirs(exeDir, 0o755) + if err != nil { + return err + } + resourceDir := filepath.Join(contentsDirectory, "/Resources") + err = fs.MkDirs(resourceDir, 0o755) + if err != nil { + return err + } + // Copy binary + packedBinaryPath := filepath.Join(exeDir, options.ProjectData.OutputFilename) + err = fs.MoveFile(options.CompiledBinary, packedBinaryPath) + if err != nil { + return errors.Wrap(err, "Cannot move file: "+options.CompiledBinary) + } + + // Generate Info.plist + err = processPList(options, contentsDirectory) + if err != nil { + return err + } + + // Generate App Icon + err = processDarwinIcon(options.ProjectData, "appicon", resourceDir, "iconfile") + if err != nil { + return err + } + + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir, "") + if err != nil { + return err + } + } + + options.CompiledBinary = packedBinaryPath + + return nil +} + +func processPList(options *Options, contentsDirectory string) error { + sourcePList := "Info.plist" + if options.Mode == Dev { + // Use Info.dev.plist if using build mode + sourcePList = "Info.dev.plist" + } + + // Read the resolved BuildAssets file and copy it to the destination + content, err := buildassets.ReadFileWithProjectData(options.ProjectData, "darwin/"+sourcePList) + if err != nil { + return err + } + + targetFile := filepath.Join(contentsDirectory, "Info.plist") + return os.WriteFile(targetFile, content, 0o644) +} + +func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string, destIconName string) (err error) { + appIcon, err := buildassets.ReadFile(projectData, iconName+".png") + if err != nil { + return err + } + + srcImg, _, err := image.Decode(bytes.NewBuffer(appIcon)) + if err != nil { + return err + } + + if destIconName == "" { + destIconName = iconName + } + + tgtBundle := filepath.Join(resourceDir, destIconName+".icns") + dest, err := os.Create(tgtBundle) + if err != nil { + return err + } + defer func() { + err = dest.Close() + if err == nil { + return + } + }() + return icns.Encode(dest, srcImg) +} + +func packageApplicationForWindows(options *Options) error { + // Generate app icon + var err error + err = generateIcoFile(options, "appicon", "icon") + if err != nil { + return err + } + + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = generateIcoFile(options, fileAssociation.IconName, "") + if err != nil { + return err + } + } + + // Create syso file + err = compileResources(options) + if err != nil { + return err + } + + return nil +} + +func packageApplicationForLinux(_ *Options) error { + return nil +} + +func generateIcoFile(options *Options, iconName string, destIconName string) error { + content, err := buildassets.ReadFile(options.ProjectData, iconName+".png") + if err != nil { + return err + } + + if destIconName == "" { + destIconName = iconName + } + + // Check ico file exists already + icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+destIconName+".ico") + if !fs.FileExists(icoFile) { + if dir := filepath.Dir(icoFile); !fs.DirExists(dir) { + if err := fs.MkDirs(dir, 0o755); err != nil { + return err + } + } + + output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer output.Close() + + err = winicon.GenerateIcon(bytes.NewBuffer(content), output, []int{256, 128, 64, 48, 32, 16}) + if err != nil { + return err + } + } + return nil +} + +func compileResources(options *Options) error { + currentDir, err := os.Getwd() + if err != nil { + return err + } + defer func() { + _ = os.Chdir(currentDir) + }() + windowsDir := filepath.Join(options.ProjectData.GetBuildDir(), "windows") + err = os.Chdir(windowsDir) + if err != nil { + return err + } + rs := winres.ResourceSet{} + icon := filepath.Join(windowsDir, "icon.ico") + iconFile, err := os.Open(icon) + if err != nil { + return err + } + defer iconFile.Close() + ico, err := winres.LoadICO(iconFile) + if err != nil { + return fmt.Errorf("couldn't load icon from icon.ico: %w", err) + } + err = rs.SetIcon(winres.RT_ICON, ico) + if err != nil { + return err + } + + manifestData, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/wails.exe.manifest") + if err != nil { + return err + } + + xmlData, err := winres.AppManifestFromXML(manifestData) + if err != nil { + return err + } + rs.SetManifest(xmlData) + + versionInfo, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/info.json") + if err != nil { + return err + } + + if len(versionInfo) != 0 { + var v version.Info + if err := v.UnmarshalJSON(versionInfo); err != nil { + return err + } + rs.SetVersionInfo(v) + } + + // replace spaces with underscores as go build behaves weirdly with spaces in syso filename + targetFile := filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso") + fout, err := os.Create(targetFile) + if err != nil { + return err + } + defer fout.Close() + + archs := map[string]winres.Arch{ + "amd64": winres.ArchAMD64, + "arm64": winres.ArchARM64, + "386": winres.ArchI386, + } + targetArch, supported := archs[options.Arch] + if !supported { + return fmt.Errorf("arch '%s' not supported", options.Arch) + } + + err = rs.WriteObject(fout, targetArch) + if err != nil { + return err + } + return nil +} diff --git a/v2/pkg/commands/build/packager_linux.go b/v2/pkg/commands/build/packager_linux.go new file mode 100644 index 000000000..6b297d3ec --- /dev/null +++ b/v2/pkg/commands/build/packager_linux.go @@ -0,0 +1,5 @@ +package build + +func packageApplication(_ *Options) error { + return nil +} diff --git a/v2/pkg/commands/buildtags/buildtags.go b/v2/pkg/commands/buildtags/buildtags.go new file mode 100644 index 000000000..5cca16acf --- /dev/null +++ b/v2/pkg/commands/buildtags/buildtags.go @@ -0,0 +1,52 @@ +package buildtags + +import ( + "errors" + "strings" + + "github.com/samber/lo" +) + +// Parse parses the given tags string and returns +// a cleaned slice of strings. Both comma and space delimited +// tags are supported but not mixed. If mixed, an error is returned. +func Parse(tags string) ([]string, error) { + if tags == "" { + return nil, nil + } + + separator := "" + if strings.Contains(tags, ",") { + separator = "," + } + if strings.Contains(tags, " ") { + if separator != "" { + return nil, errors.New("cannot use both space and comma separated values with `-tags` flag") + } + separator = " " + } + if separator == "" { + // We couldn't find any separator, so the whole string is used as user tag + // Otherwise we would end up with a list of every single character of the tags string, + // e.g.: `t,e,s,t` + return []string{tags}, nil + } + + var userTags []string + for _, tag := range strings.Split(tags, separator) { + thisTag := strings.TrimSpace(tag) + if thisTag != "" { + userTags = append(userTags, thisTag) + } + } + return userTags, nil +} + +// Stringify converts the given tags slice to a string compatible +// with the go build -tags flag +func Stringify(tags []string) string { + tags = lo.Map(tags, func(tag string, _ int) string { + return strings.TrimSpace(tag) + }) + return strings.Join(tags, ",") +} diff --git a/v2/pkg/commands/buildtags/buildtags_test.go b/v2/pkg/commands/buildtags/buildtags_test.go new file mode 100644 index 000000000..c13a158c7 --- /dev/null +++ b/v2/pkg/commands/buildtags/buildtags_test.go @@ -0,0 +1,78 @@ +package buildtags + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + tags string + want []string + wantErr bool + }{ + { + name: "should support single tags", + tags: "test", + want: []string{"test"}, + wantErr: false, + }, + { + name: "should support space delimited tags", + tags: "test test2", + want: []string{"test", "test2"}, + wantErr: false, + }, + { + name: "should support comma delimited tags", + tags: "test,test2", + want: []string{"test", "test2"}, + wantErr: false, + }, + { + name: "should error if mixed tags", + tags: "test,test2 test3", + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.tags) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStringify(t *testing.T) { + tests := []struct { + name string + tags []string + want string + }{ + { + name: "should support single tags", + tags: []string{"test"}, + want: "test", + }, + { + name: "should support multiple tags", + tags: []string{"test", "test2"}, + want: "test,test2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Stringify(tt.tags); got != tt.want { + t.Errorf("Stringify() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/git/git.go b/v2/pkg/git/git.go new file mode 100644 index 000000000..a0ac68ca9 --- /dev/null +++ b/v2/pkg/git/git.go @@ -0,0 +1,64 @@ +package git + +import ( + "encoding/json" + "fmt" + "runtime" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +func gitcommand() string { + gitcommand := "git" + if runtime.GOOS == "windows" { + gitcommand = "git.exe" + } + + return gitcommand +} + +// IsInstalled returns true if git is installed for the given platform +func IsInstalled() bool { + return shell.CommandExists(gitcommand()) +} + +// Email tries to retrieve the +func Email() (string, error) { + stdout, _, err := shell.RunCommand(".", gitcommand(), "config", "user.email") + return stdout, err +} + +// Name tries to retrieve the +func Name() (string, error) { + errMsg := "failed to retrieve git user name: %w" + stdout, _, err := shell.RunCommand(".", gitcommand(), "config", "user.name") + if err != nil { + return "", fmt.Errorf(errMsg, err) + } + name := strings.TrimSpace(stdout) + return EscapeName(name) +} + +func EscapeName(str string) (string, error) { + b, err := json.Marshal(str) + if err != nil { + return "", err + } + // Remove the surrounding quotes + escaped := string(b[1 : len(b)-1]) + + // Check if username is JSON compliant + var js json.RawMessage + jsonVal := fmt.Sprintf(`{"name": "%s"}`, escaped) + err = json.Unmarshal([]byte(jsonVal), &js) + if err != nil { + return "", fmt.Errorf("failed to retrieve git user name: %w", err) + } + return escaped, nil +} + +func InitRepo(projectDir string) error { + _, _, err := shell.RunCommand(projectDir, gitcommand(), "init") + return err +} diff --git a/v2/pkg/git/git_test.go b/v2/pkg/git/git_test.go new file mode 100644 index 000000000..238008ec3 --- /dev/null +++ b/v2/pkg/git/git_test.go @@ -0,0 +1,44 @@ +package git + +import ( + "testing" +) + +func TestEscapeName1(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Escape Apostrophe", + args: args{ + str: `John O'Keefe`, + }, + want: `John O'Keefe`, + }, + { + name: "Escape backslash", + args: args{ + str: `MYDOMAIN\USER`, + }, + want: `MYDOMAIN\\USER`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := EscapeName(tt.args.str) + if (err != nil) != tt.wantErr { + t.Errorf("EscapeName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("EscapeName() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/logger/default.go b/v2/pkg/logger/default.go new file mode 100644 index 000000000..ac83d4f2f --- /dev/null +++ b/v2/pkg/logger/default.go @@ -0,0 +1,49 @@ +package logger + +import ( + "os" +) + +// DefaultLogger is a utility to log messages to a number of destinations +type DefaultLogger struct{} + +// NewDefaultLogger creates a new Logger. +func NewDefaultLogger() Logger { + return &DefaultLogger{} +} + +// Print works like Sprintf. +func (l *DefaultLogger) Print(message string) { + println(message) +} + +// Trace level logging. Works like Sprintf. +func (l *DefaultLogger) Trace(message string) { + println("TRA | " + message) +} + +// Debug level logging. Works like Sprintf. +func (l *DefaultLogger) Debug(message string) { + println("DEB | " + message) +} + +// Info level logging. Works like Sprintf. +func (l *DefaultLogger) Info(message string) { + println("INF | " + message) +} + +// Warning level logging. Works like Sprintf. +func (l *DefaultLogger) Warning(message string) { + println("WAR | " + message) +} + +// Error level logging. Works like Sprintf. +func (l *DefaultLogger) Error(message string) { + println("ERR | " + message) +} + +// Fatal level logging. Works like Sprintf. +func (l *DefaultLogger) Fatal(message string) { + println("FAT | " + message) + os.Exit(1) +} diff --git a/v2/pkg/logger/filelogger.go b/v2/pkg/logger/filelogger.go new file mode 100644 index 000000000..954c46f59 --- /dev/null +++ b/v2/pkg/logger/filelogger.go @@ -0,0 +1,66 @@ +package logger + +import ( + "log" + "os" +) + +// FileLogger is a utility to log messages to a number of destinations +type FileLogger struct { + filename string +} + +// NewFileLogger creates a new Logger. +func NewFileLogger(filename string) Logger { + return &FileLogger{ + filename: filename, + } +} + +// Print works like Sprintf. +func (l *FileLogger) Print(message string) { + f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + log.Fatal(err) + } + if _, err := f.WriteString(message); err != nil { + f.Close() + log.Fatal(err) + } + f.Close() +} + +func (l *FileLogger) Println(message string) { + l.Print(message + "\n") +} + +// Trace level logging. Works like Sprintf. +func (l *FileLogger) Trace(message string) { + l.Println("TRACE | " + message) +} + +// Debug level logging. Works like Sprintf. +func (l *FileLogger) Debug(message string) { + l.Println("DEBUG | " + message) +} + +// Info level logging. Works like Sprintf. +func (l *FileLogger) Info(message string) { + l.Println("INFO | " + message) +} + +// Warning level logging. Works like Sprintf. +func (l *FileLogger) Warning(message string) { + l.Println("WARN | " + message) +} + +// Error level logging. Works like Sprintf. +func (l *FileLogger) Error(message string) { + l.Println("ERROR | " + message) +} + +// Fatal level logging. Works like Sprintf. +func (l *FileLogger) Fatal(message string) { + l.Println("FATAL | " + message) + os.Exit(1) +} diff --git a/v2/pkg/logger/logger.go b/v2/pkg/logger/logger.go new file mode 100644 index 000000000..990dffe75 --- /dev/null +++ b/v2/pkg/logger/logger.go @@ -0,0 +1,72 @@ +package logger + +import ( + "fmt" + "strings" +) + +// LogLevel is an unsigned 8bit int +type LogLevel uint8 + +const ( + // TRACE level + TRACE LogLevel = 1 + + // DEBUG level logging + DEBUG LogLevel = 2 + + // INFO level logging + INFO LogLevel = 3 + + // WARNING level logging + WARNING LogLevel = 4 + + // ERROR level logging + ERROR LogLevel = 5 +) + +var logLevelMap = map[string]LogLevel{ + "trace": TRACE, + "debug": DEBUG, + "info": INFO, + "warning": WARNING, + "error": ERROR, +} + +func StringToLogLevel(input string) (LogLevel, error) { + result, ok := logLevelMap[strings.ToLower(input)] + if !ok { + return ERROR, fmt.Errorf("invalid log level: %s", input) + } + return result, nil +} + +// String returns the string representation of the LogLevel +func (l LogLevel) String() string { + switch l { + case TRACE: + return "trace" + case DEBUG: + return "debug" + case INFO: + return "info" + case WARNING: + return "warning" + case ERROR: + return "error" + default: + return "debug" + } +} + +// Logger specifies the methods required to attach +// a logger to a Wails application +type Logger interface { + Print(message string) + Trace(message string) + Debug(message string) + Info(message string) + Warning(message string) + Error(message string) + Fatal(message string) +} diff --git a/v2/pkg/mac/login_darwin.go b/v2/pkg/mac/login_darwin.go new file mode 100644 index 000000000..b2390e305 --- /dev/null +++ b/v2/pkg/mac/login_darwin.go @@ -0,0 +1,60 @@ +// Package mac provides MacOS related utility functions for Wails applications +package mac + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/leaanthony/slicer" + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/shell" +) + +// StartAtLogin will either add or remove this application to/from the login +// items, depending on the given boolean flag. The limitation is that the +// currently running app must be in an app bundle. +func StartAtLogin(enabled bool) error { + exe, err := os.Executable() + if err != nil { + return errors.Wrap(err, "Error running os.Executable:") + } + binName := filepath.Base(exe) + if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) { + return fmt.Errorf("app needs to be running as package.app file to start at login") + } + appPath := strings.TrimSuffix(exe, "/Contents/MacOS/"+binName) + var command string + if enabled { + command = fmt.Sprintf("tell application \"System Events\" to make login item at end with properties {name: \"%s\",path:\"%s\", hidden:false}", binName, appPath) + } else { + command = fmt.Sprintf("tell application \"System Events\" to delete login item \"%s\"", binName) + } + _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command) + if err != nil { + return errors.Wrap(err, stde) + } + return nil +} + +// StartsAtLogin will indicate if this application is in the login +// items. The limitation is that the currently running app must be +// in an app bundle. +func StartsAtLogin() (bool, error) { + exe, err := os.Executable() + if err != nil { + return false, err + } + binName := filepath.Base(exe) + if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) { + return false, fmt.Errorf("app needs to be running as package.app file to start at login") + } + results, stde, err := shell.RunCommand("/tmp", "osascript", "-e", `tell application "System Events" to get the name of every login item`) + if err != nil { + return false, errors.Wrap(err, stde) + } + results = strings.TrimSpace(results) + startupApps := slicer.String(strings.Split(results, ", ")) + return startupApps.Contains(binName), nil +} diff --git a/v2/pkg/mac/notification_darwin.go b/v2/pkg/mac/notification_darwin.go new file mode 100644 index 000000000..243f07c78 --- /dev/null +++ b/v2/pkg/mac/notification_darwin.go @@ -0,0 +1,30 @@ +// Package mac provides MacOS related utility functions for Wails applications +package mac + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/shell" +) + +// ShowNotification will either add or remove this application to/from the login +// items, depending on the given boolean flag. The limitation is that the +// currently running app must be in an app bundle. +func ShowNotification(title string, subtitle string, message string, sound string) error { + command := fmt.Sprintf("display notification \"%s\"", message) + if len(title) > 0 { + command += fmt.Sprintf(" with title \"%s\"", title) + } + if len(subtitle) > 0 { + command += fmt.Sprintf(" subtitle \"%s\"", subtitle) + } + if len(sound) > 0 { + command += fmt.Sprintf(" sound name \"%s\"", sound) + } + _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command) + if err != nil { + return errors.Wrap(err, stde) + } + return nil +} diff --git a/v2/pkg/mac/notification_darwin_test.go b/v2/pkg/mac/notification_darwin_test.go new file mode 100644 index 000000000..7d14c00e5 --- /dev/null +++ b/v2/pkg/mac/notification_darwin_test.go @@ -0,0 +1,34 @@ +package mac + +import "testing" + +func TestShowNotification(t *testing.T) { + type args struct { + title string + subtitle string + message string + sound string + } + tests := []struct { + name string + title string + subtitle string + message string + sound string + wantErr bool + }{ + {"No message", "", "", "", "", false}, + {"Title only", "I am a Title", "", "", "", false}, + {"SubTitle only", "", "I am a subtitle", "", "", false}, + {"Message only", "", "", "I am a message!", "", false}, + {"Sound only", "", "", "", "submarine.aiff", false}, + {"Full", "Title", "Subtitle", "This is a long message to show that text gets wrapped in a notification", "submarine.aiff", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ShowNotification(tt.title, tt.subtitle, tt.message, tt.sound); (err != nil) != tt.wantErr { + t.Errorf("ShowNotification() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/v2/pkg/menu/README.md b/v2/pkg/menu/README.md new file mode 100644 index 000000000..7c66a1051 --- /dev/null +++ b/v2/pkg/menu/README.md @@ -0,0 +1,10 @@ +# Menus + +Menu support is heavily inspired by Electron's approach. + +## Features + + * Supports Text, Checkbox, Radio, Submenu and Separator + * Radio groups are defined as any number of adjacent radio items + * UTF-8 menu labels + * UTF-8 menu IDs \ No newline at end of file diff --git a/v2/pkg/menu/callback.go b/v2/pkg/menu/callback.go new file mode 100644 index 000000000..a02664ac0 --- /dev/null +++ b/v2/pkg/menu/callback.go @@ -0,0 +1,8 @@ +package menu + +type CallbackData struct { + MenuItem *MenuItem + // ContextData string +} + +type Callback func(*CallbackData) diff --git a/v2/pkg/menu/colours/colours.go b/v2/pkg/menu/colours/colours.go new file mode 100644 index 000000000..5fb74eabd --- /dev/null +++ b/v2/pkg/menu/colours/colours.go @@ -0,0 +1,68 @@ +package main + +import ( + "bytes" + _ "embed" + "encoding/json" + "io" + "log" + "net/http" + "os" + "path/filepath" + "text/template" +) + +type Rgb struct { + R uint8 `json:"r"` + G uint8 `json:"g"` + B uint8 `json:"b"` +} + +type Hsl struct { + H float64 `json:"h"` + S float64 `json:"s"` + L float64 `json:"l"` +} + +type InputCol struct { + Colorid uint8 `json:"colorId"` + Hexstring string `json:"hexString"` + Rgb Rgb `json:"rgb"` + Hsl Hsl `json:"hsl"` + Name string `json:"name"` +} + +//go:embed gen.tmpl +var Template string + +func main() { + var Cols []InputCol + + resp, err := http.Get("https://jonasjacek.github.io/colors/data.json") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(data, &Cols) + if err != nil { + log.Fatal(err) + } + + t, err := template.New("cols").Parse(Template) + if err != nil { + log.Fatal(err) + } + var buffer bytes.Buffer + err = t.Execute(&buffer, Cols) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile(filepath.Join("..", "cols.go"), buffer.Bytes(), 0o755) + if err != nil { + log.Fatal(err) + } +} diff --git a/v2/pkg/menu/colours/gen.tmpl b/v2/pkg/menu/colours/gen.tmpl new file mode 100644 index 000000000..bd5d1ff3c --- /dev/null +++ b/v2/pkg/menu/colours/gen.tmpl @@ -0,0 +1,29 @@ +package menu + +type Rgb struct { + R uint8 `json:"r"` + G uint8 `json:"g"` + B uint8 `json:"b"` +} + +type Hsl struct { + H float64 `json:"h"` + S float64 `json:"s"` + L float64 `json:"l"` +} + +type Col struct { + Hex string `json:"hex"` + Rgb Rgb `json:"rgb"` + Hsl Hsl `json:"hsl"` + Name string `json:"name"` +} + +var Cols = []*Col{ {{range $col := .}} + { + Hex: "{{.Hexstring}}", + Rgb: Rgb{ {{.Rgb.R}}, {{.Rgb.G}}, {{.Rgb.B}} }, + Hsl: Hsl{ {{.Hsl.H}}, {{.Hsl.S}}, {{.Hsl.L}} }, + Name: "{{.Name}}", + },{{end}} +} diff --git a/v2/pkg/menu/cols.go b/v2/pkg/menu/cols.go new file mode 100755 index 000000000..738e4baaf --- /dev/null +++ b/v2/pkg/menu/cols.go @@ -0,0 +1,1559 @@ +package menu + +type Rgb struct { + R uint8 `json:"r"` + G uint8 `json:"g"` + B uint8 `json:"b"` +} + +type Hsl struct { + H float64 `json:"h"` + S float64 `json:"s"` + L float64 `json:"l"` +} + +type Col struct { + Hex string `json:"hex"` + Rgb Rgb `json:"rgb"` + Hsl Hsl `json:"hsl"` + Name string `json:"name"` +} + +var Cols = []*Col{ + { + Hex: "#000000", + Rgb: Rgb{0, 0, 0}, + Hsl: Hsl{0, 0, 0}, + Name: "Black", + }, + { + Hex: "#800000", + Rgb: Rgb{128, 0, 0}, + Hsl: Hsl{0, 100, 25}, + Name: "Maroon", + }, + { + Hex: "#008000", + Rgb: Rgb{0, 128, 0}, + Hsl: Hsl{120, 100, 25}, + Name: "Green", + }, + { + Hex: "#808000", + Rgb: Rgb{128, 128, 0}, + Hsl: Hsl{60, 100, 25}, + Name: "Olive", + }, + { + Hex: "#000080", + Rgb: Rgb{0, 0, 128}, + Hsl: Hsl{240, 100, 25}, + Name: "Navy", + }, + { + Hex: "#800080", + Rgb: Rgb{128, 0, 128}, + Hsl: Hsl{300, 100, 25}, + Name: "Purple", + }, + { + Hex: "#008080", + Rgb: Rgb{0, 128, 128}, + Hsl: Hsl{180, 100, 25}, + Name: "Teal", + }, + { + Hex: "#c0c0c0", + Rgb: Rgb{192, 192, 192}, + Hsl: Hsl{0, 0, 75}, + Name: "Silver", + }, + { + Hex: "#808080", + Rgb: Rgb{128, 128, 128}, + Hsl: Hsl{0, 0, 50}, + Name: "Grey", + }, + { + Hex: "#ff0000", + Rgb: Rgb{255, 0, 0}, + Hsl: Hsl{0, 100, 50}, + Name: "Red", + }, + { + Hex: "#00ff00", + Rgb: Rgb{0, 255, 0}, + Hsl: Hsl{120, 100, 50}, + Name: "Lime", + }, + { + Hex: "#ffff00", + Rgb: Rgb{255, 255, 0}, + Hsl: Hsl{60, 100, 50}, + Name: "Yellow", + }, + { + Hex: "#0000ff", + Rgb: Rgb{0, 0, 255}, + Hsl: Hsl{240, 100, 50}, + Name: "Blue", + }, + { + Hex: "#ff00ff", + Rgb: Rgb{255, 0, 255}, + Hsl: Hsl{300, 100, 50}, + Name: "Fuchsia", + }, + { + Hex: "#00ffff", + Rgb: Rgb{0, 255, 255}, + Hsl: Hsl{180, 100, 50}, + Name: "Aqua", + }, + { + Hex: "#ffffff", + Rgb: Rgb{255, 255, 255}, + Hsl: Hsl{0, 0, 100}, + Name: "White", + }, + { + Hex: "#000000", + Rgb: Rgb{0, 0, 0}, + Hsl: Hsl{0, 0, 0}, + Name: "Grey0", + }, + { + Hex: "#00005f", + Rgb: Rgb{0, 0, 95}, + Hsl: Hsl{240, 100, 18}, + Name: "NavyBlue", + }, + { + Hex: "#000087", + Rgb: Rgb{0, 0, 135}, + Hsl: Hsl{240, 100, 26}, + Name: "DarkBlue", + }, + { + Hex: "#0000af", + Rgb: Rgb{0, 0, 175}, + Hsl: Hsl{240, 100, 34}, + Name: "Blue3", + }, + { + Hex: "#0000d7", + Rgb: Rgb{0, 0, 215}, + Hsl: Hsl{240, 100, 42}, + Name: "Blue3", + }, + { + Hex: "#0000ff", + Rgb: Rgb{0, 0, 255}, + Hsl: Hsl{240, 100, 50}, + Name: "Blue1", + }, + { + Hex: "#005f00", + Rgb: Rgb{0, 95, 0}, + Hsl: Hsl{120, 100, 18}, + Name: "DarkGreen", + }, + { + Hex: "#005f5f", + Rgb: Rgb{0, 95, 95}, + Hsl: Hsl{180, 100, 18}, + Name: "DeepSkyBlue4", + }, + { + Hex: "#005f87", + Rgb: Rgb{0, 95, 135}, + Hsl: Hsl{197.777777777778, 100, 26}, + Name: "DeepSkyBlue4", + }, + { + Hex: "#005faf", + Rgb: Rgb{0, 95, 175}, + Hsl: Hsl{207.428571428571, 100, 34}, + Name: "DeepSkyBlue4", + }, + { + Hex: "#005fd7", + Rgb: Rgb{0, 95, 215}, + Hsl: Hsl{213.488372093023, 100, 42}, + Name: "DodgerBlue3", + }, + { + Hex: "#005fff", + Rgb: Rgb{0, 95, 255}, + Hsl: Hsl{217.647058823529, 100, 50}, + Name: "DodgerBlue2", + }, + { + Hex: "#008700", + Rgb: Rgb{0, 135, 0}, + Hsl: Hsl{120, 100, 26}, + Name: "Green4", + }, + { + Hex: "#00875f", + Rgb: Rgb{0, 135, 95}, + Hsl: Hsl{162.222222222222, 100, 26}, + Name: "SpringGreen4", + }, + { + Hex: "#008787", + Rgb: Rgb{0, 135, 135}, + Hsl: Hsl{180, 100, 26}, + Name: "Turquoise4", + }, + { + Hex: "#0087af", + Rgb: Rgb{0, 135, 175}, + Hsl: Hsl{193.714285714286, 100, 34}, + Name: "DeepSkyBlue3", + }, + { + Hex: "#0087d7", + Rgb: Rgb{0, 135, 215}, + Hsl: Hsl{202.325581395349, 100, 42}, + Name: "DeepSkyBlue3", + }, + { + Hex: "#0087ff", + Rgb: Rgb{0, 135, 255}, + Hsl: Hsl{208.235294117647, 100, 50}, + Name: "DodgerBlue1", + }, + { + Hex: "#00af00", + Rgb: Rgb{0, 175, 0}, + Hsl: Hsl{120, 100, 34}, + Name: "Green3", + }, + { + Hex: "#00af5f", + Rgb: Rgb{0, 175, 95}, + Hsl: Hsl{152.571428571429, 100, 34}, + Name: "SpringGreen3", + }, + { + Hex: "#00af87", + Rgb: Rgb{0, 175, 135}, + Hsl: Hsl{166.285714285714, 100, 34}, + Name: "DarkCyan", + }, + { + Hex: "#00afaf", + Rgb: Rgb{0, 175, 175}, + Hsl: Hsl{180, 100, 34}, + Name: "LightSeaGreen", + }, + { + Hex: "#00afd7", + Rgb: Rgb{0, 175, 215}, + Hsl: Hsl{191.162790697674, 100, 42}, + Name: "DeepSkyBlue2", + }, + { + Hex: "#00afff", + Rgb: Rgb{0, 175, 255}, + Hsl: Hsl{198.823529411765, 100, 50}, + Name: "DeepSkyBlue1", + }, + { + Hex: "#00d700", + Rgb: Rgb{0, 215, 0}, + Hsl: Hsl{120, 100, 42}, + Name: "Green3", + }, + { + Hex: "#00d75f", + Rgb: Rgb{0, 215, 95}, + Hsl: Hsl{146.511627906977, 100, 42}, + Name: "SpringGreen3", + }, + { + Hex: "#00d787", + Rgb: Rgb{0, 215, 135}, + Hsl: Hsl{157.674418604651, 100, 42}, + Name: "SpringGreen2", + }, + { + Hex: "#00d7af", + Rgb: Rgb{0, 215, 175}, + Hsl: Hsl{168.837209302326, 100, 42}, + Name: "Cyan3", + }, + { + Hex: "#00d7d7", + Rgb: Rgb{0, 215, 215}, + Hsl: Hsl{180, 100, 42}, + Name: "DarkTurquoise", + }, + { + Hex: "#00d7ff", + Rgb: Rgb{0, 215, 255}, + Hsl: Hsl{189.411764705882, 100, 50}, + Name: "Turquoise2", + }, + { + Hex: "#00ff00", + Rgb: Rgb{0, 255, 0}, + Hsl: Hsl{120, 100, 50}, + Name: "Green1", + }, + { + Hex: "#00ff5f", + Rgb: Rgb{0, 255, 95}, + Hsl: Hsl{142.352941176471, 100, 50}, + Name: "SpringGreen2", + }, + { + Hex: "#00ff87", + Rgb: Rgb{0, 255, 135}, + Hsl: Hsl{151.764705882353, 100, 50}, + Name: "SpringGreen1", + }, + { + Hex: "#00ffaf", + Rgb: Rgb{0, 255, 175}, + Hsl: Hsl{161.176470588235, 100, 50}, + Name: "MediumSpringGreen", + }, + { + Hex: "#00ffd7", + Rgb: Rgb{0, 255, 215}, + Hsl: Hsl{170.588235294118, 100, 50}, + Name: "Cyan2", + }, + { + Hex: "#00ffff", + Rgb: Rgb{0, 255, 255}, + Hsl: Hsl{180, 100, 50}, + Name: "Cyan1", + }, + { + Hex: "#5f0000", + Rgb: Rgb{95, 0, 0}, + Hsl: Hsl{0, 100, 18}, + Name: "DarkRed", + }, + { + Hex: "#5f005f", + Rgb: Rgb{95, 0, 95}, + Hsl: Hsl{300, 100, 18}, + Name: "DeepPink4", + }, + { + Hex: "#5f0087", + Rgb: Rgb{95, 0, 135}, + Hsl: Hsl{282.222222222222, 100, 26}, + Name: "Purple4", + }, + { + Hex: "#5f00af", + Rgb: Rgb{95, 0, 175}, + Hsl: Hsl{272.571428571429, 100, 34}, + Name: "Purple4", + }, + { + Hex: "#5f00d7", + Rgb: Rgb{95, 0, 215}, + Hsl: Hsl{266.511627906977, 100, 42}, + Name: "Purple3", + }, + { + Hex: "#5f00ff", + Rgb: Rgb{95, 0, 255}, + Hsl: Hsl{262.352941176471, 100, 50}, + Name: "BlueViolet", + }, + { + Hex: "#5f5f00", + Rgb: Rgb{95, 95, 0}, + Hsl: Hsl{60, 100, 18}, + Name: "Orange4", + }, + { + Hex: "#5f5f5f", + Rgb: Rgb{95, 95, 95}, + Hsl: Hsl{0, 0, 37}, + Name: "Grey37", + }, + { + Hex: "#5f5f87", + Rgb: Rgb{95, 95, 135}, + Hsl: Hsl{240, 17, 45}, + Name: "MediumPurple4", + }, + { + Hex: "#5f5faf", + Rgb: Rgb{95, 95, 175}, + Hsl: Hsl{240, 33, 52}, + Name: "SlateBlue3", + }, + { + Hex: "#5f5fd7", + Rgb: Rgb{95, 95, 215}, + Hsl: Hsl{240, 60, 60}, + Name: "SlateBlue3", + }, + { + Hex: "#5f5fff", + Rgb: Rgb{95, 95, 255}, + Hsl: Hsl{240, 100, 68}, + Name: "RoyalBlue1", + }, + { + Hex: "#5f8700", + Rgb: Rgb{95, 135, 0}, + Hsl: Hsl{77.7777777777778, 100, 26}, + Name: "Chartreuse4", + }, + { + Hex: "#5f875f", + Rgb: Rgb{95, 135, 95}, + Hsl: Hsl{120, 17, 45}, + Name: "DarkSeaGreen4", + }, + { + Hex: "#5f8787", + Rgb: Rgb{95, 135, 135}, + Hsl: Hsl{180, 17, 45}, + Name: "PaleTurquoise4", + }, + { + Hex: "#5f87af", + Rgb: Rgb{95, 135, 175}, + Hsl: Hsl{210, 33, 52}, + Name: "SteelBlue", + }, + { + Hex: "#5f87d7", + Rgb: Rgb{95, 135, 215}, + Hsl: Hsl{220, 60, 60}, + Name: "SteelBlue3", + }, + { + Hex: "#5f87ff", + Rgb: Rgb{95, 135, 255}, + Hsl: Hsl{225, 100, 68}, + Name: "CornflowerBlue", + }, + { + Hex: "#5faf00", + Rgb: Rgb{95, 175, 0}, + Hsl: Hsl{87.4285714285714, 100, 34}, + Name: "Chartreuse3", + }, + { + Hex: "#5faf5f", + Rgb: Rgb{95, 175, 95}, + Hsl: Hsl{120, 33, 52}, + Name: "DarkSeaGreen4", + }, + { + Hex: "#5faf87", + Rgb: Rgb{95, 175, 135}, + Hsl: Hsl{150, 33, 52}, + Name: "CadetBlue", + }, + { + Hex: "#5fafaf", + Rgb: Rgb{95, 175, 175}, + Hsl: Hsl{180, 33, 52}, + Name: "CadetBlue", + }, + { + Hex: "#5fafd7", + Rgb: Rgb{95, 175, 215}, + Hsl: Hsl{200, 60, 60}, + Name: "SkyBlue3", + }, + { + Hex: "#5fafff", + Rgb: Rgb{95, 175, 255}, + Hsl: Hsl{210, 100, 68}, + Name: "SteelBlue1", + }, + { + Hex: "#5fd700", + Rgb: Rgb{95, 215, 0}, + Hsl: Hsl{93.4883720930233, 100, 42}, + Name: "Chartreuse3", + }, + { + Hex: "#5fd75f", + Rgb: Rgb{95, 215, 95}, + Hsl: Hsl{120, 60, 60}, + Name: "PaleGreen3", + }, + { + Hex: "#5fd787", + Rgb: Rgb{95, 215, 135}, + Hsl: Hsl{140, 60, 60}, + Name: "SeaGreen3", + }, + { + Hex: "#5fd7af", + Rgb: Rgb{95, 215, 175}, + Hsl: Hsl{160, 60, 60}, + Name: "Aquamarine3", + }, + { + Hex: "#5fd7d7", + Rgb: Rgb{95, 215, 215}, + Hsl: Hsl{180, 60, 60}, + Name: "MediumTurquoise", + }, + { + Hex: "#5fd7ff", + Rgb: Rgb{95, 215, 255}, + Hsl: Hsl{195, 100, 68}, + Name: "SteelBlue1", + }, + { + Hex: "#5fff00", + Rgb: Rgb{95, 255, 0}, + Hsl: Hsl{97.6470588235294, 100, 50}, + Name: "Chartreuse2", + }, + { + Hex: "#5fff5f", + Rgb: Rgb{95, 255, 95}, + Hsl: Hsl{120, 100, 68}, + Name: "SeaGreen2", + }, + { + Hex: "#5fff87", + Rgb: Rgb{95, 255, 135}, + Hsl: Hsl{135, 100, 68}, + Name: "SeaGreen1", + }, + { + Hex: "#5fffaf", + Rgb: Rgb{95, 255, 175}, + Hsl: Hsl{150, 100, 68}, + Name: "SeaGreen1", + }, + { + Hex: "#5fffd7", + Rgb: Rgb{95, 255, 215}, + Hsl: Hsl{165, 100, 68}, + Name: "Aquamarine1", + }, + { + Hex: "#5fffff", + Rgb: Rgb{95, 255, 255}, + Hsl: Hsl{180, 100, 68}, + Name: "DarkSlateGray2", + }, + { + Hex: "#870000", + Rgb: Rgb{135, 0, 0}, + Hsl: Hsl{0, 100, 26}, + Name: "DarkRed", + }, + { + Hex: "#87005f", + Rgb: Rgb{135, 0, 95}, + Hsl: Hsl{317.777777777778, 100, 26}, + Name: "DeepPink4", + }, + { + Hex: "#870087", + Rgb: Rgb{135, 0, 135}, + Hsl: Hsl{300, 100, 26}, + Name: "DarkMagenta", + }, + { + Hex: "#8700af", + Rgb: Rgb{135, 0, 175}, + Hsl: Hsl{286.285714285714, 100, 34}, + Name: "DarkMagenta", + }, + { + Hex: "#8700d7", + Rgb: Rgb{135, 0, 215}, + Hsl: Hsl{277.674418604651, 100, 42}, + Name: "DarkViolet", + }, + { + Hex: "#8700ff", + Rgb: Rgb{135, 0, 255}, + Hsl: Hsl{271.764705882353, 100, 50}, + Name: "Purple", + }, + { + Hex: "#875f00", + Rgb: Rgb{135, 95, 0}, + Hsl: Hsl{42.2222222222222, 100, 26}, + Name: "Orange4", + }, + { + Hex: "#875f5f", + Rgb: Rgb{135, 95, 95}, + Hsl: Hsl{0, 17, 45}, + Name: "LightPink4", + }, + { + Hex: "#875f87", + Rgb: Rgb{135, 95, 135}, + Hsl: Hsl{300, 17, 45}, + Name: "Plum4", + }, + { + Hex: "#875faf", + Rgb: Rgb{135, 95, 175}, + Hsl: Hsl{270, 33, 52}, + Name: "MediumPurple3", + }, + { + Hex: "#875fd7", + Rgb: Rgb{135, 95, 215}, + Hsl: Hsl{260, 60, 60}, + Name: "MediumPurple3", + }, + { + Hex: "#875fff", + Rgb: Rgb{135, 95, 255}, + Hsl: Hsl{255, 100, 68}, + Name: "SlateBlue1", + }, + { + Hex: "#878700", + Rgb: Rgb{135, 135, 0}, + Hsl: Hsl{60, 100, 26}, + Name: "Yellow4", + }, + { + Hex: "#87875f", + Rgb: Rgb{135, 135, 95}, + Hsl: Hsl{60, 17, 45}, + Name: "Wheat4", + }, + { + Hex: "#878787", + Rgb: Rgb{135, 135, 135}, + Hsl: Hsl{0, 0, 52}, + Name: "Grey53", + }, + { + Hex: "#8787af", + Rgb: Rgb{135, 135, 175}, + Hsl: Hsl{240, 20, 60}, + Name: "LightSlateGrey", + }, + { + Hex: "#8787d7", + Rgb: Rgb{135, 135, 215}, + Hsl: Hsl{240, 50, 68}, + Name: "MediumPurple", + }, + { + Hex: "#8787ff", + Rgb: Rgb{135, 135, 255}, + Hsl: Hsl{240, 100, 76}, + Name: "LightSlateBlue", + }, + { + Hex: "#87af00", + Rgb: Rgb{135, 175, 0}, + Hsl: Hsl{73.7142857142857, 100, 34}, + Name: "Yellow4", + }, + { + Hex: "#87af5f", + Rgb: Rgb{135, 175, 95}, + Hsl: Hsl{90, 33, 52}, + Name: "DarkOliveGreen3", + }, + { + Hex: "#87af87", + Rgb: Rgb{135, 175, 135}, + Hsl: Hsl{120, 20, 60}, + Name: "DarkSeaGreen", + }, + { + Hex: "#87afaf", + Rgb: Rgb{135, 175, 175}, + Hsl: Hsl{180, 20, 60}, + Name: "LightSkyBlue3", + }, + { + Hex: "#87afd7", + Rgb: Rgb{135, 175, 215}, + Hsl: Hsl{210, 50, 68}, + Name: "LightSkyBlue3", + }, + { + Hex: "#87afff", + Rgb: Rgb{135, 175, 255}, + Hsl: Hsl{220, 100, 76}, + Name: "SkyBlue2", + }, + { + Hex: "#87d700", + Rgb: Rgb{135, 215, 0}, + Hsl: Hsl{82.3255813953488, 100, 42}, + Name: "Chartreuse2", + }, + { + Hex: "#87d75f", + Rgb: Rgb{135, 215, 95}, + Hsl: Hsl{100, 60, 60}, + Name: "DarkOliveGreen3", + }, + { + Hex: "#87d787", + Rgb: Rgb{135, 215, 135}, + Hsl: Hsl{120, 50, 68}, + Name: "PaleGreen3", + }, + { + Hex: "#87d7af", + Rgb: Rgb{135, 215, 175}, + Hsl: Hsl{150, 50, 68}, + Name: "DarkSeaGreen3", + }, + { + Hex: "#87d7d7", + Rgb: Rgb{135, 215, 215}, + Hsl: Hsl{180, 50, 68}, + Name: "DarkSlateGray3", + }, + { + Hex: "#87d7ff", + Rgb: Rgb{135, 215, 255}, + Hsl: Hsl{200, 100, 76}, + Name: "SkyBlue1", + }, + { + Hex: "#87ff00", + Rgb: Rgb{135, 255, 0}, + Hsl: Hsl{88.2352941176471, 100, 50}, + Name: "Chartreuse1", + }, + { + Hex: "#87ff5f", + Rgb: Rgb{135, 255, 95}, + Hsl: Hsl{105, 100, 68}, + Name: "LightGreen", + }, + { + Hex: "#87ff87", + Rgb: Rgb{135, 255, 135}, + Hsl: Hsl{120, 100, 76}, + Name: "LightGreen", + }, + { + Hex: "#87ffaf", + Rgb: Rgb{135, 255, 175}, + Hsl: Hsl{140, 100, 76}, + Name: "PaleGreen1", + }, + { + Hex: "#87ffd7", + Rgb: Rgb{135, 255, 215}, + Hsl: Hsl{160, 100, 76}, + Name: "Aquamarine1", + }, + { + Hex: "#87ffff", + Rgb: Rgb{135, 255, 255}, + Hsl: Hsl{180, 100, 76}, + Name: "DarkSlateGray1", + }, + { + Hex: "#af0000", + Rgb: Rgb{175, 0, 0}, + Hsl: Hsl{0, 100, 34}, + Name: "Red3", + }, + { + Hex: "#af005f", + Rgb: Rgb{175, 0, 95}, + Hsl: Hsl{327.428571428571, 100, 34}, + Name: "DeepPink4", + }, + { + Hex: "#af0087", + Rgb: Rgb{175, 0, 135}, + Hsl: Hsl{313.714285714286, 100, 34}, + Name: "MediumVioletRed", + }, + { + Hex: "#af00af", + Rgb: Rgb{175, 0, 175}, + Hsl: Hsl{300, 100, 34}, + Name: "Magenta3", + }, + { + Hex: "#af00d7", + Rgb: Rgb{175, 0, 215}, + Hsl: Hsl{288.837209302326, 100, 42}, + Name: "DarkViolet", + }, + { + Hex: "#af00ff", + Rgb: Rgb{175, 0, 255}, + Hsl: Hsl{281.176470588235, 100, 50}, + Name: "Purple", + }, + { + Hex: "#af5f00", + Rgb: Rgb{175, 95, 0}, + Hsl: Hsl{32.5714285714286, 100, 34}, + Name: "DarkOrange3", + }, + { + Hex: "#af5f5f", + Rgb: Rgb{175, 95, 95}, + Hsl: Hsl{0, 33, 52}, + Name: "IndianRed", + }, + { + Hex: "#af5f87", + Rgb: Rgb{175, 95, 135}, + Hsl: Hsl{330, 33, 52}, + Name: "HotPink3", + }, + { + Hex: "#af5faf", + Rgb: Rgb{175, 95, 175}, + Hsl: Hsl{300, 33, 52}, + Name: "MediumOrchid3", + }, + { + Hex: "#af5fd7", + Rgb: Rgb{175, 95, 215}, + Hsl: Hsl{280, 60, 60}, + Name: "MediumOrchid", + }, + { + Hex: "#af5fff", + Rgb: Rgb{175, 95, 255}, + Hsl: Hsl{270, 100, 68}, + Name: "MediumPurple2", + }, + { + Hex: "#af8700", + Rgb: Rgb{175, 135, 0}, + Hsl: Hsl{46.2857142857143, 100, 34}, + Name: "DarkGoldenrod", + }, + { + Hex: "#af875f", + Rgb: Rgb{175, 135, 95}, + Hsl: Hsl{30, 33, 52}, + Name: "LightSalmon3", + }, + { + Hex: "#af8787", + Rgb: Rgb{175, 135, 135}, + Hsl: Hsl{0, 20, 60}, + Name: "RosyBrown", + }, + { + Hex: "#af87af", + Rgb: Rgb{175, 135, 175}, + Hsl: Hsl{300, 20, 60}, + Name: "Grey63", + }, + { + Hex: "#af87d7", + Rgb: Rgb{175, 135, 215}, + Hsl: Hsl{270, 50, 68}, + Name: "MediumPurple2", + }, + { + Hex: "#af87ff", + Rgb: Rgb{175, 135, 255}, + Hsl: Hsl{260, 100, 76}, + Name: "MediumPurple1", + }, + { + Hex: "#afaf00", + Rgb: Rgb{175, 175, 0}, + Hsl: Hsl{60, 100, 34}, + Name: "Gold3", + }, + { + Hex: "#afaf5f", + Rgb: Rgb{175, 175, 95}, + Hsl: Hsl{60, 33, 52}, + Name: "DarkKhaki", + }, + { + Hex: "#afaf87", + Rgb: Rgb{175, 175, 135}, + Hsl: Hsl{60, 20, 60}, + Name: "NavajoWhite3", + }, + { + Hex: "#afafaf", + Rgb: Rgb{175, 175, 175}, + Hsl: Hsl{0, 0, 68}, + Name: "Grey69", + }, + { + Hex: "#afafd7", + Rgb: Rgb{175, 175, 215}, + Hsl: Hsl{240, 33, 76}, + Name: "LightSteelBlue3", + }, + { + Hex: "#afafff", + Rgb: Rgb{175, 175, 255}, + Hsl: Hsl{240, 100, 84}, + Name: "LightSteelBlue", + }, + { + Hex: "#afd700", + Rgb: Rgb{175, 215, 0}, + Hsl: Hsl{71.1627906976744, 100, 42}, + Name: "Yellow3", + }, + { + Hex: "#afd75f", + Rgb: Rgb{175, 215, 95}, + Hsl: Hsl{80, 60, 60}, + Name: "DarkOliveGreen3", + }, + { + Hex: "#afd787", + Rgb: Rgb{175, 215, 135}, + Hsl: Hsl{90, 50, 68}, + Name: "DarkSeaGreen3", + }, + { + Hex: "#afd7af", + Rgb: Rgb{175, 215, 175}, + Hsl: Hsl{120, 33, 76}, + Name: "DarkSeaGreen2", + }, + { + Hex: "#afd7d7", + Rgb: Rgb{175, 215, 215}, + Hsl: Hsl{180, 33, 76}, + Name: "LightCyan3", + }, + { + Hex: "#afd7ff", + Rgb: Rgb{175, 215, 255}, + Hsl: Hsl{210, 100, 84}, + Name: "LightSkyBlue1", + }, + { + Hex: "#afff00", + Rgb: Rgb{175, 255, 0}, + Hsl: Hsl{78.8235294117647, 100, 50}, + Name: "GreenYellow", + }, + { + Hex: "#afff5f", + Rgb: Rgb{175, 255, 95}, + Hsl: Hsl{90, 100, 68}, + Name: "DarkOliveGreen2", + }, + { + Hex: "#afff87", + Rgb: Rgb{175, 255, 135}, + Hsl: Hsl{100, 100, 76}, + Name: "PaleGreen1", + }, + { + Hex: "#afffaf", + Rgb: Rgb{175, 255, 175}, + Hsl: Hsl{120, 100, 84}, + Name: "DarkSeaGreen2", + }, + { + Hex: "#afffd7", + Rgb: Rgb{175, 255, 215}, + Hsl: Hsl{150, 100, 84}, + Name: "DarkSeaGreen1", + }, + { + Hex: "#afffff", + Rgb: Rgb{175, 255, 255}, + Hsl: Hsl{180, 100, 84}, + Name: "PaleTurquoise1", + }, + { + Hex: "#d70000", + Rgb: Rgb{215, 0, 0}, + Hsl: Hsl{0, 100, 42}, + Name: "Red3", + }, + { + Hex: "#d7005f", + Rgb: Rgb{215, 0, 95}, + Hsl: Hsl{333.488372093023, 100, 42}, + Name: "DeepPink3", + }, + { + Hex: "#d70087", + Rgb: Rgb{215, 0, 135}, + Hsl: Hsl{322.325581395349, 100, 42}, + Name: "DeepPink3", + }, + { + Hex: "#d700af", + Rgb: Rgb{215, 0, 175}, + Hsl: Hsl{311.162790697674, 100, 42}, + Name: "Magenta3", + }, + { + Hex: "#d700d7", + Rgb: Rgb{215, 0, 215}, + Hsl: Hsl{300, 100, 42}, + Name: "Magenta3", + }, + { + Hex: "#d700ff", + Rgb: Rgb{215, 0, 255}, + Hsl: Hsl{290.588235294118, 100, 50}, + Name: "Magenta2", + }, + { + Hex: "#d75f00", + Rgb: Rgb{215, 95, 0}, + Hsl: Hsl{26.5116279069767, 100, 42}, + Name: "DarkOrange3", + }, + { + Hex: "#d75f5f", + Rgb: Rgb{215, 95, 95}, + Hsl: Hsl{0, 60, 60}, + Name: "IndianRed", + }, + { + Hex: "#d75f87", + Rgb: Rgb{215, 95, 135}, + Hsl: Hsl{340, 60, 60}, + Name: "HotPink3", + }, + { + Hex: "#d75faf", + Rgb: Rgb{215, 95, 175}, + Hsl: Hsl{320, 60, 60}, + Name: "HotPink2", + }, + { + Hex: "#d75fd7", + Rgb: Rgb{215, 95, 215}, + Hsl: Hsl{300, 60, 60}, + Name: "Orchid", + }, + { + Hex: "#d75fff", + Rgb: Rgb{215, 95, 255}, + Hsl: Hsl{285, 100, 68}, + Name: "MediumOrchid1", + }, + { + Hex: "#d78700", + Rgb: Rgb{215, 135, 0}, + Hsl: Hsl{37.6744186046512, 100, 42}, + Name: "Orange3", + }, + { + Hex: "#d7875f", + Rgb: Rgb{215, 135, 95}, + Hsl: Hsl{20, 60, 60}, + Name: "LightSalmon3", + }, + { + Hex: "#d78787", + Rgb: Rgb{215, 135, 135}, + Hsl: Hsl{0, 50, 68}, + Name: "LightPink3", + }, + { + Hex: "#d787af", + Rgb: Rgb{215, 135, 175}, + Hsl: Hsl{330, 50, 68}, + Name: "Pink3", + }, + { + Hex: "#d787d7", + Rgb: Rgb{215, 135, 215}, + Hsl: Hsl{300, 50, 68}, + Name: "Plum3", + }, + { + Hex: "#d787ff", + Rgb: Rgb{215, 135, 255}, + Hsl: Hsl{280, 100, 76}, + Name: "Violet", + }, + { + Hex: "#d7af00", + Rgb: Rgb{215, 175, 0}, + Hsl: Hsl{48.8372093023256, 100, 42}, + Name: "Gold3", + }, + { + Hex: "#d7af5f", + Rgb: Rgb{215, 175, 95}, + Hsl: Hsl{40, 60, 60}, + Name: "LightGoldenrod3", + }, + { + Hex: "#d7af87", + Rgb: Rgb{215, 175, 135}, + Hsl: Hsl{30, 50, 68}, + Name: "Tan", + }, + { + Hex: "#d7afaf", + Rgb: Rgb{215, 175, 175}, + Hsl: Hsl{0, 33, 76}, + Name: "MistyRose3", + }, + { + Hex: "#d7afd7", + Rgb: Rgb{215, 175, 215}, + Hsl: Hsl{300, 33, 76}, + Name: "Thistle3", + }, + { + Hex: "#d7afff", + Rgb: Rgb{215, 175, 255}, + Hsl: Hsl{270, 100, 84}, + Name: "Plum2", + }, + { + Hex: "#d7d700", + Rgb: Rgb{215, 215, 0}, + Hsl: Hsl{60, 100, 42}, + Name: "Yellow3", + }, + { + Hex: "#d7d75f", + Rgb: Rgb{215, 215, 95}, + Hsl: Hsl{60, 60, 60}, + Name: "Khaki3", + }, + { + Hex: "#d7d787", + Rgb: Rgb{215, 215, 135}, + Hsl: Hsl{60, 50, 68}, + Name: "LightGoldenrod2", + }, + { + Hex: "#d7d7af", + Rgb: Rgb{215, 215, 175}, + Hsl: Hsl{60, 33, 76}, + Name: "LightYellow3", + }, + { + Hex: "#d7d7d7", + Rgb: Rgb{215, 215, 215}, + Hsl: Hsl{0, 0, 84}, + Name: "Grey84", + }, + { + Hex: "#d7d7ff", + Rgb: Rgb{215, 215, 255}, + Hsl: Hsl{240, 100, 92}, + Name: "LightSteelBlue1", + }, + { + Hex: "#d7ff00", + Rgb: Rgb{215, 255, 0}, + Hsl: Hsl{69.4117647058823, 100, 50}, + Name: "Yellow2", + }, + { + Hex: "#d7ff5f", + Rgb: Rgb{215, 255, 95}, + Hsl: Hsl{75, 100, 68}, + Name: "DarkOliveGreen1", + }, + { + Hex: "#d7ff87", + Rgb: Rgb{215, 255, 135}, + Hsl: Hsl{80, 100, 76}, + Name: "DarkOliveGreen1", + }, + { + Hex: "#d7ffaf", + Rgb: Rgb{215, 255, 175}, + Hsl: Hsl{90, 100, 84}, + Name: "DarkSeaGreen1", + }, + { + Hex: "#d7ffd7", + Rgb: Rgb{215, 255, 215}, + Hsl: Hsl{120, 100, 92}, + Name: "Honeydew2", + }, + { + Hex: "#d7ffff", + Rgb: Rgb{215, 255, 255}, + Hsl: Hsl{180, 100, 92}, + Name: "LightCyan1", + }, + { + Hex: "#ff0000", + Rgb: Rgb{255, 0, 0}, + Hsl: Hsl{0, 100, 50}, + Name: "Red1", + }, + { + Hex: "#ff005f", + Rgb: Rgb{255, 0, 95}, + Hsl: Hsl{337.647058823529, 100, 50}, + Name: "DeepPink2", + }, + { + Hex: "#ff0087", + Rgb: Rgb{255, 0, 135}, + Hsl: Hsl{328.235294117647, 100, 50}, + Name: "DeepPink1", + }, + { + Hex: "#ff00af", + Rgb: Rgb{255, 0, 175}, + Hsl: Hsl{318.823529411765, 100, 50}, + Name: "DeepPink1", + }, + { + Hex: "#ff00d7", + Rgb: Rgb{255, 0, 215}, + Hsl: Hsl{309.411764705882, 100, 50}, + Name: "Magenta2", + }, + { + Hex: "#ff00ff", + Rgb: Rgb{255, 0, 255}, + Hsl: Hsl{300, 100, 50}, + Name: "Magenta1", + }, + { + Hex: "#ff5f00", + Rgb: Rgb{255, 95, 0}, + Hsl: Hsl{22.3529411764706, 100, 50}, + Name: "OrangeRed1", + }, + { + Hex: "#ff5f5f", + Rgb: Rgb{255, 95, 95}, + Hsl: Hsl{0, 100, 68}, + Name: "IndianRed1", + }, + { + Hex: "#ff5f87", + Rgb: Rgb{255, 95, 135}, + Hsl: Hsl{345, 100, 68}, + Name: "IndianRed1", + }, + { + Hex: "#ff5faf", + Rgb: Rgb{255, 95, 175}, + Hsl: Hsl{330, 100, 68}, + Name: "HotPink", + }, + { + Hex: "#ff5fd7", + Rgb: Rgb{255, 95, 215}, + Hsl: Hsl{315, 100, 68}, + Name: "HotPink", + }, + { + Hex: "#ff5fff", + Rgb: Rgb{255, 95, 255}, + Hsl: Hsl{300, 100, 68}, + Name: "MediumOrchid1", + }, + { + Hex: "#ff8700", + Rgb: Rgb{255, 135, 0}, + Hsl: Hsl{31.7647058823529, 100, 50}, + Name: "DarkOrange", + }, + { + Hex: "#ff875f", + Rgb: Rgb{255, 135, 95}, + Hsl: Hsl{15, 100, 68}, + Name: "Salmon1", + }, + { + Hex: "#ff8787", + Rgb: Rgb{255, 135, 135}, + Hsl: Hsl{0, 100, 76}, + Name: "LightCoral", + }, + { + Hex: "#ff87af", + Rgb: Rgb{255, 135, 175}, + Hsl: Hsl{340, 100, 76}, + Name: "PaleVioletRed1", + }, + { + Hex: "#ff87d7", + Rgb: Rgb{255, 135, 215}, + Hsl: Hsl{320, 100, 76}, + Name: "Orchid2", + }, + { + Hex: "#ff87ff", + Rgb: Rgb{255, 135, 255}, + Hsl: Hsl{300, 100, 76}, + Name: "Orchid1", + }, + { + Hex: "#ffaf00", + Rgb: Rgb{255, 175, 0}, + Hsl: Hsl{41.1764705882353, 100, 50}, + Name: "Orange1", + }, + { + Hex: "#ffaf5f", + Rgb: Rgb{255, 175, 95}, + Hsl: Hsl{30, 100, 68}, + Name: "SandyBrown", + }, + { + Hex: "#ffaf87", + Rgb: Rgb{255, 175, 135}, + Hsl: Hsl{20, 100, 76}, + Name: "LightSalmon1", + }, + { + Hex: "#ffafaf", + Rgb: Rgb{255, 175, 175}, + Hsl: Hsl{0, 100, 84}, + Name: "LightPink1", + }, + { + Hex: "#ffafd7", + Rgb: Rgb{255, 175, 215}, + Hsl: Hsl{330, 100, 84}, + Name: "Pink1", + }, + { + Hex: "#ffafff", + Rgb: Rgb{255, 175, 255}, + Hsl: Hsl{300, 100, 84}, + Name: "Plum1", + }, + { + Hex: "#ffd700", + Rgb: Rgb{255, 215, 0}, + Hsl: Hsl{50.5882352941176, 100, 50}, + Name: "Gold1", + }, + { + Hex: "#ffd75f", + Rgb: Rgb{255, 215, 95}, + Hsl: Hsl{45, 100, 68}, + Name: "LightGoldenrod2", + }, + { + Hex: "#ffd787", + Rgb: Rgb{255, 215, 135}, + Hsl: Hsl{40, 100, 76}, + Name: "LightGoldenrod2", + }, + { + Hex: "#ffd7af", + Rgb: Rgb{255, 215, 175}, + Hsl: Hsl{30, 100, 84}, + Name: "NavajoWhite1", + }, + { + Hex: "#ffd7d7", + Rgb: Rgb{255, 215, 215}, + Hsl: Hsl{0, 100, 92}, + Name: "MistyRose1", + }, + { + Hex: "#ffd7ff", + Rgb: Rgb{255, 215, 255}, + Hsl: Hsl{300, 100, 92}, + Name: "Thistle1", + }, + { + Hex: "#ffff00", + Rgb: Rgb{255, 255, 0}, + Hsl: Hsl{60, 100, 50}, + Name: "Yellow1", + }, + { + Hex: "#ffff5f", + Rgb: Rgb{255, 255, 95}, + Hsl: Hsl{60, 100, 68}, + Name: "LightGoldenrod1", + }, + { + Hex: "#ffff87", + Rgb: Rgb{255, 255, 135}, + Hsl: Hsl{60, 100, 76}, + Name: "Khaki1", + }, + { + Hex: "#ffffaf", + Rgb: Rgb{255, 255, 175}, + Hsl: Hsl{60, 100, 84}, + Name: "Wheat1", + }, + { + Hex: "#ffffd7", + Rgb: Rgb{255, 255, 215}, + Hsl: Hsl{60, 100, 92}, + Name: "Cornsilk1", + }, + { + Hex: "#ffffff", + Rgb: Rgb{255, 255, 255}, + Hsl: Hsl{0, 0, 100}, + Name: "Grey100", + }, + { + Hex: "#080808", + Rgb: Rgb{8, 8, 8}, + Hsl: Hsl{0, 0, 3}, + Name: "Grey3", + }, + { + Hex: "#121212", + Rgb: Rgb{18, 18, 18}, + Hsl: Hsl{0, 0, 7}, + Name: "Grey7", + }, + { + Hex: "#1c1c1c", + Rgb: Rgb{28, 28, 28}, + Hsl: Hsl{0, 0, 10}, + Name: "Grey11", + }, + { + Hex: "#262626", + Rgb: Rgb{38, 38, 38}, + Hsl: Hsl{0, 0, 14}, + Name: "Grey15", + }, + { + Hex: "#303030", + Rgb: Rgb{48, 48, 48}, + Hsl: Hsl{0, 0, 18}, + Name: "Grey19", + }, + { + Hex: "#3a3a3a", + Rgb: Rgb{58, 58, 58}, + Hsl: Hsl{0, 0, 22}, + Name: "Grey23", + }, + { + Hex: "#444444", + Rgb: Rgb{68, 68, 68}, + Hsl: Hsl{0, 0, 26}, + Name: "Grey27", + }, + { + Hex: "#4e4e4e", + Rgb: Rgb{78, 78, 78}, + Hsl: Hsl{0, 0, 30}, + Name: "Grey30", + }, + { + Hex: "#585858", + Rgb: Rgb{88, 88, 88}, + Hsl: Hsl{0, 0, 34}, + Name: "Grey35", + }, + { + Hex: "#626262", + Rgb: Rgb{98, 98, 98}, + Hsl: Hsl{0, 0, 37}, + Name: "Grey39", + }, + { + Hex: "#6c6c6c", + Rgb: Rgb{108, 108, 108}, + Hsl: Hsl{0, 0, 40}, + Name: "Grey42", + }, + { + Hex: "#767676", + Rgb: Rgb{118, 118, 118}, + Hsl: Hsl{0, 0, 46}, + Name: "Grey46", + }, + { + Hex: "#808080", + Rgb: Rgb{128, 128, 128}, + Hsl: Hsl{0, 0, 50}, + Name: "Grey50", + }, + { + Hex: "#8a8a8a", + Rgb: Rgb{138, 138, 138}, + Hsl: Hsl{0, 0, 54}, + Name: "Grey54", + }, + { + Hex: "#949494", + Rgb: Rgb{148, 148, 148}, + Hsl: Hsl{0, 0, 58}, + Name: "Grey58", + }, + { + Hex: "#9e9e9e", + Rgb: Rgb{158, 158, 158}, + Hsl: Hsl{0, 0, 61}, + Name: "Grey62", + }, + { + Hex: "#a8a8a8", + Rgb: Rgb{168, 168, 168}, + Hsl: Hsl{0, 0, 65}, + Name: "Grey66", + }, + { + Hex: "#b2b2b2", + Rgb: Rgb{178, 178, 178}, + Hsl: Hsl{0, 0, 69}, + Name: "Grey70", + }, + { + Hex: "#bcbcbc", + Rgb: Rgb{188, 188, 188}, + Hsl: Hsl{0, 0, 73}, + Name: "Grey74", + }, + { + Hex: "#c6c6c6", + Rgb: Rgb{198, 198, 198}, + Hsl: Hsl{0, 0, 77}, + Name: "Grey78", + }, + { + Hex: "#d0d0d0", + Rgb: Rgb{208, 208, 208}, + Hsl: Hsl{0, 0, 81}, + Name: "Grey82", + }, + { + Hex: "#dadada", + Rgb: Rgb{218, 218, 218}, + Hsl: Hsl{0, 0, 85}, + Name: "Grey85", + }, + { + Hex: "#e4e4e4", + Rgb: Rgb{228, 228, 228}, + Hsl: Hsl{0, 0, 89}, + Name: "Grey89", + }, + { + Hex: "#eeeeee", + Rgb: Rgb{238, 238, 238}, + Hsl: Hsl{0, 0, 93}, + Name: "Grey93", + }, +} diff --git a/v2/pkg/menu/contextmenu.go b/v2/pkg/menu/contextmenu.go new file mode 100644 index 000000000..e24b04067 --- /dev/null +++ b/v2/pkg/menu/contextmenu.go @@ -0,0 +1,13 @@ +package menu + +type ContextMenu struct { + ID string + Menu *Menu +} + +func NewContextMenu(ID string, menu *Menu) *ContextMenu { + return &ContextMenu{ + ID: ID, + Menu: menu, + } +} diff --git a/v2/pkg/menu/keys/keys.go b/v2/pkg/menu/keys/keys.go new file mode 100644 index 000000000..961edab2d --- /dev/null +++ b/v2/pkg/menu/keys/keys.go @@ -0,0 +1,104 @@ +package keys + +import ( + "fmt" + "strings" +) + +// Modifier is actually a string +type Modifier string + +const ( + // CmdOrCtrlKey represents Command on Mac and Control on other platforms + CmdOrCtrlKey Modifier = "cmdorctrl" + // OptionOrAltKey represents Option on Mac and Alt on other platforms + OptionOrAltKey Modifier = "optionoralt" + // ShiftKey represents the shift key on all systems + ShiftKey Modifier = "shift" + // SuperKey represents Command on Mac and the Windows key on the other platforms + // SuperKey Modifier = "super" + // ControlKey represents the control key on all systems + ControlKey Modifier = "ctrl" +) + +var modifierMap = map[string]Modifier{ + "cmdorctrl": CmdOrCtrlKey, + "optionoralt": OptionOrAltKey, + "shift": ShiftKey, + //"super": SuperKey, + "ctrl": ControlKey, +} + +func parseModifier(text string) (*Modifier, error) { + lowertext := strings.ToLower(text) + result, valid := modifierMap[lowertext] + if !valid { + return nil, fmt.Errorf("'%s' is not a valid modifier", text) + } + + return &result, nil +} + +// Accelerator holds the keyboard shortcut for a menu item +type Accelerator struct { + Key string + Modifiers []Modifier +} + +// Key creates a standard key Accelerator +func Key(key string) *Accelerator { + return &Accelerator{ + Key: strings.ToLower(key), + } +} + +// CmdOrCtrl creates a 'CmdOrCtrl' Accelerator +func CmdOrCtrl(key string) *Accelerator { + return &Accelerator{ + Key: strings.ToLower(key), + Modifiers: []Modifier{CmdOrCtrlKey}, + } +} + +// OptionOrAlt creates a 'OptionOrAlt' Accelerator +func OptionOrAlt(key string) *Accelerator { + return &Accelerator{ + Key: strings.ToLower(key), + Modifiers: []Modifier{OptionOrAltKey}, + } +} + +// Shift creates a 'Shift' Accelerator +func Shift(key string) *Accelerator { + return &Accelerator{ + Key: strings.ToLower(key), + Modifiers: []Modifier{ShiftKey}, + } +} + +// Control creates a 'Control' Accelerator +func Control(key string) *Accelerator { + return &Accelerator{ + Key: strings.ToLower(key), + Modifiers: []Modifier{ControlKey}, + } +} + +// +//// Super creates a 'Super' Accelerator +//func Super(key string) *Accelerator { +// return &Accelerator{ +// Key: strings.ToLower(key), +// Modifiers: []Modifier{SuperKey}, +// } +//} + +// Combo creates an Accelerator with multiple Modifiers +func Combo(key string, modifier1 Modifier, modifier2 Modifier, rest ...Modifier) *Accelerator { + result := &Accelerator{ + Key: key, + Modifiers: []Modifier{modifier1, modifier2}, + } + result.Modifiers = append(result.Modifiers, rest...) + return result +} diff --git a/v2/pkg/menu/keys/macmodifiers.go b/v2/pkg/menu/keys/macmodifiers.go new file mode 100644 index 000000000..7da618b4e --- /dev/null +++ b/v2/pkg/menu/keys/macmodifiers.go @@ -0,0 +1,26 @@ +package keys + +const ( + NSEventModifierFlagShift = 1 << 17 // Set if Shift key is pressed. + NSEventModifierFlagControl = 1 << 18 // Set if Control key is pressed. + NSEventModifierFlagOption = 1 << 19 // Set if Option or Alternate key is pressed. + NSEventModifierFlagCommand = 1 << 20 // Set if Command key is pressed. +) + +var macModifierMap = map[Modifier]int{ + CmdOrCtrlKey: NSEventModifierFlagCommand, + ControlKey: NSEventModifierFlagControl, + OptionOrAltKey: NSEventModifierFlagOption, + ShiftKey: NSEventModifierFlagShift, +} + +func ToMacModifier(accelerator *Accelerator) int { + if accelerator == nil { + return 0 + } + result := 0 + for _, modifier := range accelerator.Modifiers { + result |= macModifierMap[modifier] + } + return result +} diff --git a/v2/pkg/menu/keys/macmodifiers_test.go b/v2/pkg/menu/keys/macmodifiers_test.go new file mode 100644 index 000000000..8be1bd05a --- /dev/null +++ b/v2/pkg/menu/keys/macmodifiers_test.go @@ -0,0 +1,31 @@ +package keys + +import "testing" + +func TestToMacModifier(t *testing.T) { + + tests := []struct { + name string + accelerator *Accelerator + want int + }{ + // TODO: Add test cases. + {"nil", nil, 0}, + {"empty", &Accelerator{}, 0}, + {"key", &Accelerator{Key: "p"}, 0}, + {"cmd", CmdOrCtrl(""), NSEventModifierFlagCommand}, + {"ctrl", Control(""), NSEventModifierFlagControl}, + {"shift", Shift(""), NSEventModifierFlagShift}, + {"option", OptionOrAlt(""), NSEventModifierFlagOption}, + {"cmd+ctrl", Combo("", CmdOrCtrlKey, ControlKey), NSEventModifierFlagCommand | NSEventModifierFlagControl}, + {"cmd+ctrl+shift", Combo("", CmdOrCtrlKey, ControlKey, ShiftKey), NSEventModifierFlagCommand | NSEventModifierFlagControl | NSEventModifierFlagShift}, + {"cmd+ctrl+shift+option", Combo("", CmdOrCtrlKey, ControlKey, ShiftKey, OptionOrAltKey), NSEventModifierFlagCommand | NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagOption}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToMacModifier(tt.accelerator); got != tt.want { + t.Errorf("ToMacModifier() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/menu/keys/parser.go b/v2/pkg/menu/keys/parser.go new file mode 100644 index 000000000..6e8e12376 --- /dev/null +++ b/v2/pkg/menu/keys/parser.go @@ -0,0 +1,87 @@ +package keys + +import ( + "fmt" + "strconv" + "strings" + + "github.com/leaanthony/slicer" +) + +var namedKeys = slicer.String([]string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"}) + +func parseKey(key string) (string, bool) { + // Lowercase! + key = strings.ToLower(key) + + // Check special case + if key == "plus" { + return "+", true + } + + // Handle named keys + if namedKeys.Contains(key) { + return key, true + } + + // Check we only have a single character + if len(key) != 1 { + return "", false + } + + runeKey := rune(key[0]) + + // This may be too inclusive + if strconv.IsPrint(runeKey) { + return key, true + } + + return "", false +} + +func Parse(shortcut string) (*Accelerator, error) { + var result Accelerator + + // Split the shortcut by + + components := strings.Split(shortcut, "+") + + // If we only have one it should be a key + // We require components + if len(components) == 0 { + return nil, fmt.Errorf("no components given to validateComponents") + } + + // Keep track of modifiers we have processed + var modifiersProcessed slicer.StringSlicer + + // Check components + for index, component := range components { + + // If last component + if index == len(components)-1 { + processedkey, validKey := parseKey(component) + if !validKey { + return nil, fmt.Errorf("'%s' is not a valid key", component) + } + result.Key = processedkey + continue + } + + // Not last component - needs to be modifier + lowercaseComponent := strings.ToLower(component) + thisModifier, valid := modifierMap[lowercaseComponent] + if !valid { + return nil, fmt.Errorf("'%s' is not a valid modifier", component) + } + // Needs to be unique + if modifiersProcessed.Contains(lowercaseComponent) { + return nil, fmt.Errorf("Modifier '%s' is defined twice for shortcut: %s", component, shortcut) + } + + // Save this data + result.Modifiers = append(result.Modifiers, thisModifier) + modifiersProcessed.Add(lowercaseComponent) + } + + return &result, nil +} diff --git a/v2/pkg/menu/keys/parser_test.go b/v2/pkg/menu/keys/parser_test.go new file mode 100644 index 000000000..f63f22bed --- /dev/null +++ b/v2/pkg/menu/keys/parser_test.go @@ -0,0 +1,37 @@ +package keys + +import ( + "testing" + + "github.com/matryer/is" +) + +func TestParse(t *testing.T) { + + i := is.New(t) + + type args struct { + Input string + Expected *Accelerator + } + + gooddata := []args{ + {"CmdOrCtrl+A", CmdOrCtrl("A")}, + {"SHIFT+.", Shift(".")}, + {"CTRL+plus", Control("+")}, + {"CTRL+SHIFT+escApe", Combo("escape", ControlKey, ShiftKey)}, + {";", Key(";")}, + {"OptionOrAlt+Page Down", OptionOrAlt("Page Down")}, + } + for _, tt := range gooddata { + result, err := Parse(tt.Input) + i.NoErr(err) + i.Equal(result, tt.Expected) + } + baddata := []string{"CmdOrCrl+A", "SHIT+.", "CTL+plus", "CTRL+SHIF+esApe", "escap", "Sper+Tab", "OptionOrAlt"} + for _, d := range baddata { + result, err := Parse(d) + i.True(err != nil) + i.Equal(result, nil) + } +} diff --git a/v2/pkg/menu/keys/stringify.go b/v2/pkg/menu/keys/stringify.go new file mode 100644 index 000000000..92498f5d4 --- /dev/null +++ b/v2/pkg/menu/keys/stringify.go @@ -0,0 +1,41 @@ +package keys + +import ( + "strings" + + "github.com/leaanthony/slicer" +) + +var modifierStringMap = map[string]map[Modifier]string{ + "windows": { + CmdOrCtrlKey: "Ctrl", + ControlKey: "Ctrl", + OptionOrAltKey: "Alt", + ShiftKey: "Shift", + // SuperKey: "Win", + }, + "darwin": { + CmdOrCtrlKey: "Cmd", + ControlKey: "Ctrl", + OptionOrAltKey: "Option", + ShiftKey: "Shift", + // SuperKey: "Cmd", + }, + "linux": { + CmdOrCtrlKey: "Ctrl", + ControlKey: "Ctrl", + OptionOrAltKey: "Alt", + ShiftKey: "Shift", + // SuperKey: "Super", + }, +} + +func Stringify(accelerator *Accelerator, platform string) string { + result := slicer.String() + for _, modifier := range accelerator.Modifiers { + result.Add(modifierStringMap[platform][modifier]) + } + result.Deduplicate() + result.Add(strings.ToUpper(accelerator.Key)) + return result.Join("+") +} diff --git a/v2/pkg/menu/keys/stringify_test.go b/v2/pkg/menu/keys/stringify_test.go new file mode 100644 index 000000000..e6ba26221 --- /dev/null +++ b/v2/pkg/menu/keys/stringify_test.go @@ -0,0 +1,75 @@ +package keys + +import ( + "strconv" + "testing" +) + +func TestStringify(t *testing.T) { + + const Windows = "windows" + const Mac = "darwin" + const Linux = "linux" + tests := []struct { + arg *Accelerator + want string + platform string + }{ + // Single Keys + {Key("a"), "A", Windows}, + {Key(""), "", Windows}, + {Key("?"), "?", Windows}, + {Key("a"), "A", Mac}, + {Key(""), "", Mac}, + {Key("?"), "?", Mac}, + {Key("a"), "A", Linux}, + {Key(""), "", Linux}, + {Key("?"), "?", Linux}, + + // Single modifier + {Control("a"), "Ctrl+A", Windows}, + {Control("a"), "Ctrl+A", Mac}, + {Control("a"), "Ctrl+A", Linux}, + {CmdOrCtrl("a"), "Ctrl+A", Windows}, + {CmdOrCtrl("a"), "Cmd+A", Mac}, + {CmdOrCtrl("a"), "Ctrl+A", Linux}, + {Shift("a"), "Shift+A", Windows}, + {Shift("a"), "Shift+A", Mac}, + {Shift("a"), "Shift+A", Linux}, + {OptionOrAlt("a"), "Alt+A", Windows}, + {OptionOrAlt("a"), "Option+A", Mac}, + {OptionOrAlt("a"), "Alt+A", Linux}, + //{Super("a"), "Win+A", Windows}, + //{Super("a"), "Cmd+A", Mac}, + //{Super("a"), "Super+A", Linux}, + + // Dual Combo non duplicate + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows}, + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Option+A", Mac}, + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Cmd+Option+A", Mac}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Windows}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Option+A", Mac}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Linux}, + //{Combo("a", SuperKey, OptionOrAltKey), "Win+Alt+A", Windows}, + //{Combo("a", SuperKey, OptionOrAltKey), "Cmd+Option+A", Mac}, + //{Combo("a", SuperKey, OptionOrAltKey), "Super+Alt+A", Linux}, + + // Combo duplicate + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Windows}, + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Option+A", Mac}, + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Linux}, + //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Win+A", Windows}, + //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Option+Cmd+A", Mac}, + //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Super+A", Linux}, + } + for index, tt := range tests { + t.Run(strconv.Itoa(index), func(t *testing.T) { + if got := Stringify(tt.arg, tt.platform); got != tt.want { + t.Errorf("Stringify() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/menu/mac.go b/v2/pkg/menu/mac.go new file mode 100644 index 000000000..0889e5c42 --- /dev/null +++ b/v2/pkg/menu/mac.go @@ -0,0 +1,12 @@ +package menu + +/* +// DefaultMacMenu returns a default menu including the default +// Application and Edit menus. Use `.Append()` to add to it. +func DefaultMacMenu() *Menu { + return NewMenuFromItems( + AppMenu(), + EditMenu(), + ) +} +*/ diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go new file mode 100644 index 000000000..86acbd1d0 --- /dev/null +++ b/v2/pkg/menu/menu.go @@ -0,0 +1,75 @@ +package menu + +import "github.com/wailsapp/wails/v2/pkg/menu/keys" + +type Menu struct { + Items []*MenuItem +} + +func NewMenu() *Menu { + return &Menu{} +} + +func (m *Menu) Append(item *MenuItem) { + m.Items = append(m.Items, item) +} + +// Merge will append the items in the given menu +// into this menu +func (m *Menu) Merge(menu *Menu) { + m.Items = append(m.Items, menu.Items...) +} + +// AddText adds a TextMenu item to the menu +func (m *Menu) AddText(label string, accelerator *keys.Accelerator, click Callback) *MenuItem { + item := Text(label, accelerator, click) + m.Append(item) + return item +} + +// AddCheckbox adds a CheckboxMenu item to the menu +func (m *Menu) AddCheckbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem { + item := Checkbox(label, checked, accelerator, click) + m.Append(item) + return item +} + +// AddRadio adds a radio item to the menu +func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem { + item := Radio(label, checked, accelerator, click) + m.Append(item) + return item +} + +// AddSeparator adds a separator to the menu +func (m *Menu) AddSeparator() { + item := Separator() + m.Append(item) +} + +func (m *Menu) AddSubmenu(label string) *Menu { + submenu := NewMenu() + item := SubMenu(label, submenu) + m.Append(item) + return submenu +} + +func (m *Menu) Prepend(item *MenuItem) { + m.Items = append([]*MenuItem{item}, m.Items...) +} + +func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu { + result := NewMenu() + result.Append(first) + for _, item := range rest { + result.Append(item) + } + + return result +} + +func (m *Menu) setParent(menuItem *MenuItem) { + for _, item := range m.Items { + item.parent = menuItem + } +} diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go new file mode 100644 index 000000000..bffc522d8 --- /dev/null +++ b/v2/pkg/menu/menuitem.go @@ -0,0 +1,329 @@ +package menu + +import ( + "sync" + + "github.com/wailsapp/wails/v2/pkg/menu/keys" +) + +// MenuItem represents a menuitem contained in a menu +type MenuItem struct { + // Label is what appears as the menu text + Label string + // Role is a predefined menu type + Role Role + // Accelerator holds a representation of a key binding + Accelerator *keys.Accelerator + // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu + Type Type + // Disabled makes the item unselectable + Disabled bool + // Hidden ensures that the item is not shown in the menu + Hidden bool + // Checked indicates if the item is selected (used by Checkbox and Radio types only) + Checked bool + // SubMenu contains a list of menu items that will be shown as a submenu + // SubMenu []*MenuItem `json:"SubMenu,omitempty"` + SubMenu *Menu + + // Callback function when menu clicked + Click Callback + /* + // Text Colour + RGBA string + + // Font + FontSize int + FontName string + + // Image - base64 image data + Image string + + // MacTemplateImage indicates that on a Mac, this image is a template image + MacTemplateImage bool + + // MacAlternate indicates that this item is an alternative to the previous menu item + MacAlternate bool + + // Tooltip + Tooltip string + */ + // This holds the menu item's parent. + parent *MenuItem + + // Used for locking when removing elements + removeLock sync.Mutex +} + +// Parent returns the parent of the menu item. +// If it is a top level menu then it returns nil. +func (m *MenuItem) Parent() *MenuItem { + return m.parent +} + +// Append will attempt to append the given menu item to +// this item's submenu items. If this menu item is not a +// submenu, then this method will not add the item and +// simply return false. +func (m *MenuItem) Append(item *MenuItem) bool { + if !m.isSubMenu() { + return false + } + item.parent = m + m.SubMenu.Append(item) + return true +} + +// Prepend will attempt to prepend the given menu item to +// this item's submenu items. If this menu item is not a +// submenu, then this method will not add the item and +// simply return false. +func (m *MenuItem) Prepend(item *MenuItem) bool { + if !m.isSubMenu() { + return false + } + item.parent = m + m.SubMenu.Prepend(item) + return true +} + +func (m *MenuItem) Remove() { + // Iterate my parent's children + m.Parent().removeChild(m) +} + +func (m *MenuItem) removeChild(item *MenuItem) { + m.removeLock.Lock() + for index, child := range m.SubMenu.Items { + if item == child { + m.SubMenu.Items = append(m.SubMenu.Items[:index], m.SubMenu.Items[index+1:]...) + } + } + m.removeLock.Unlock() +} + +// InsertAfter attempts to add the given item after this item in the parent +// menu. If there is no parent menu (we are a top level menu) then false is +// returned +func (m *MenuItem) InsertAfter(item *MenuItem) bool { + // We need to find my parent + if m.parent == nil { + return false + } + + // Get my parent to insert the item + return m.parent.insertNewItemAfterGivenItem(m, item) +} + +// InsertBefore attempts to add the given item before this item in the parent +// menu. If there is no parent menu (we are a top level menu) then false is +// returned +func (m *MenuItem) InsertBefore(item *MenuItem) bool { + // We need to find my parent + if m.parent == nil { + return false + } + + // Get my parent to insert the item + return m.parent.insertNewItemBeforeGivenItem(m, item) +} + +// insertNewItemAfterGivenItem will insert the given item after the given target +// in this item's submenu. If we are not a submenu, +// then something bad has happened :/ +func (m *MenuItem) insertNewItemAfterGivenItem(target *MenuItem, + newItem *MenuItem, +) bool { + if !m.isSubMenu() { + return false + } + + // Find the index of the target + targetIndex := m.getItemIndex(target) + if targetIndex == -1 { + return false + } + + // Insert element into slice + return m.insertItemAtIndex(targetIndex+1, newItem) +} + +// insertNewItemBeforeGivenItem will insert the given item before the given +// target in this item's submenu. If we are not a submenu, then something bad +// has happened :/ +func (m *MenuItem) insertNewItemBeforeGivenItem(target *MenuItem, + newItem *MenuItem, +) bool { + if !m.isSubMenu() { + return false + } + + // Find the index of the target + targetIndex := m.getItemIndex(target) + if targetIndex == -1 { + return false + } + + // Insert element into slice + return m.insertItemAtIndex(targetIndex, newItem) +} + +func (m *MenuItem) isSubMenu() bool { + return m.Type == SubmenuType +} + +// getItemIndex returns the index of the given target relative to this menu +func (m *MenuItem) getItemIndex(target *MenuItem) int { + // This should only be called on submenus + if !m.isSubMenu() { + return -1 + } + + // hunt down that bad boy + for index, item := range m.SubMenu.Items { + if item == target { + return index + } + } + + return -1 +} + +// insertItemAtIndex attempts to insert the given item into the submenu at +// the given index +// Credit: https://stackoverflow.com/a/61822301 +func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool { + // If index is OOB, return false + if index > len(m.SubMenu.Items) { + return false + } + + // Save parent reference + target.parent = m + + // If index is last item, then just regular append + if index == len(m.SubMenu.Items) { + m.SubMenu.Items = append(m.SubMenu.Items, target) + return true + } + + m.SubMenu.Items = append(m.SubMenu.Items[:index+1], m.SubMenu.Items[index:]...) + m.SubMenu.Items[index] = target + return true +} + +func (m *MenuItem) SetLabel(name string) { + if m.Label == name { + return + } + m.Label = name +} + +func (m *MenuItem) IsSeparator() bool { + return m.Type == SeparatorType +} + +func (m *MenuItem) IsCheckbox() bool { + return m.Type == CheckboxType +} + +func (m *MenuItem) Disable() *MenuItem { + m.Disabled = true + return m +} + +func (m *MenuItem) Enable() *MenuItem { + m.Disabled = false + return m +} + +func (m *MenuItem) OnClick(click Callback) *MenuItem { + m.Click = click + return m +} + +func (m *MenuItem) SetAccelerator(acc *keys.Accelerator) *MenuItem { + m.Accelerator = acc + return m +} + +func (m *MenuItem) SetChecked(value bool) *MenuItem { + m.Checked = value + if m.Type != RadioType { + m.Type = CheckboxType + } + return m +} + +func (m *MenuItem) Hide() *MenuItem { + m.Hidden = true + return m +} + +func (m *MenuItem) Show() *MenuItem { + m.Hidden = false + return m +} + +func (m *MenuItem) IsRadio() bool { + return m.Type == RadioType +} + +func Label(label string) *MenuItem { + return &MenuItem{ + Type: TextType, + Label: label, + } +} + +// Text is a helper to create basic Text menu items +func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem { + return &MenuItem{ + Label: label, + Type: TextType, + Accelerator: accelerator, + Click: click, + } +} + +// Separator provides a menu separator +func Separator() *MenuItem { + return &MenuItem{ + Type: SeparatorType, + } +} + +// Radio is a helper to create basic Radio menu items with an accelerator +func Radio(label string, selected bool, accelerator *keys.Accelerator, click Callback) *MenuItem { + return &MenuItem{ + Label: label, + Type: RadioType, + Checked: selected, + Accelerator: accelerator, + Click: click, + } +} + +// Checkbox is a helper to create basic Checkbox menu items +func Checkbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem { + return &MenuItem{ + Label: label, + Type: CheckboxType, + Checked: checked, + Accelerator: accelerator, + Click: click, + } +} + +// SubMenu is a helper to create Submenus +func SubMenu(label string, menu *Menu) *MenuItem { + result := &MenuItem{ + Label: label, + SubMenu: menu, + Type: SubmenuType, + } + + menu.setParent(result) + + return result +} diff --git a/v2/pkg/menu/menuroles.go b/v2/pkg/menu/menuroles.go new file mode 100644 index 000000000..bcc0657fc --- /dev/null +++ b/v2/pkg/menu/menuroles.go @@ -0,0 +1,214 @@ +// Package menu provides all the functions and structs related to menus in a Wails application. +// Heavily inspired by Electron (c) 2013-2020 Github Inc. +// Electron License: https://github.com/electron/electron/blob/master/LICENSE +package menu + +// Role is a type to identify menu roles +type Role int + +// These constants need to be kept in sync with `v2/internal/frontend/desktop/darwin/Role.h` +const ( + AppMenuRole Role = 1 + EditMenuRole = 2 + WindowMenuRole = 3 + // AboutRole Role = "about" + // UndoRole Role = "undo" + // RedoRole Role = "redo" + // CutRole Role = "cut" + // CopyRole Role = "copy" + // PasteRole Role = "paste" + // PasteAndMatchStyleRole Role = "pasteAndMatchStyle" + // SelectAllRole Role = "selectAll" + // DeleteRole Role = "delete" + // MinimizeRole Role = "minimize" + // QuitRole Role = "quit" + // TogglefullscreenRole Role = "togglefullscreen" + // FileMenuRole Role = "fileMenu" + // ViewMenuRole Role = "viewMenu" + // WindowMenuRole Role = "windowMenu" + // HideRole Role = "hide" + // HideOthersRole Role = "hideOthers" + // UnhideRole Role = "unhide" + // FrontRole Role = "front" + // ZoomRole Role = "zoom" + // WindowSubMenuRole Role = "windowSubMenu" + // HelpSubMenuRole Role = "helpSubMenu" + // SeparatorItemRole Role = "separatorItem" +) + +/* +// About provides a MenuItem with the About role +func About() *MenuItem { + return &MenuItem{ + Role: AboutRole, + } +} + +// Undo provides a MenuItem with the Undo role +func Undo() *MenuItem { + return &MenuItem{ + Role: UndoRole, + } +} + +// Redo provides a MenuItem with the Redo role +func Redo() *MenuItem { + return &MenuItem{ + Role: RedoRole, + } +} + +// Cut provides a MenuItem with the Cut role +func Cut() *MenuItem { + return &MenuItem{ + Role: CutRole, + } +} + +// Copy provides a MenuItem with the Copy role +func Copy() *MenuItem { + return &MenuItem{ + Role: CopyRole, + } +} + +// Paste provides a MenuItem with the Paste role +func Paste() *MenuItem { + return &MenuItem{ + Role: PasteRole, + } +} + +// PasteAndMatchStyle provides a MenuItem with the PasteAndMatchStyle role +func PasteAndMatchStyle() *MenuItem { + return &MenuItem{ + Role: PasteAndMatchStyleRole, + } +} + +// SelectAll provides a MenuItem with the SelectAll role +func SelectAll() *MenuItem { + return &MenuItem{ + Role: SelectAllRole, + } +} + +// Delete provides a MenuItem with the Delete role +func Delete() *MenuItem { + return &MenuItem{ + Role: DeleteRole, + } +} + +// Minimize provides a MenuItem with the Minimize role +func Minimize() *MenuItem { + return &MenuItem{ + Role: MinimizeRole, + } +} + +// Quit provides a MenuItem with the Quit role +func Quit() *MenuItem { + return &MenuItem{ + Role: QuitRole, + } +} + +// ToggleFullscreen provides a MenuItem with the ToggleFullscreen role +func ToggleFullscreen() *MenuItem { + return &MenuItem{ + Role: TogglefullscreenRole, + } +} + +// FileMenu provides a MenuItem with the whole default "File" menu (Close / Quit) +func FileMenu() *MenuItem { + return &MenuItem{ + Role: FileMenuRole, + } +} +*/ + +// EditMenu provides a MenuItem with the whole default "Edit" menu (Undo, Copy, etc.). +func EditMenu() *MenuItem { + return &MenuItem{ + Role: EditMenuRole, + } +} + +/* +// ViewMenu provides a MenuItem with the whole default "View" menu (Reload, Toggle Developer Tools, etc.) +func ViewMenu() *MenuItem { + return &MenuItem{ + Role: ViewMenuRole, + } +} +*/ + +// WindowMenu provides a MenuItem with the whole default "Window" menu (Minimize, Zoom, etc.). +// On MacOS currently all options in there won't work if the window is frameless. +func WindowMenu() *MenuItem { + return &MenuItem{ + Role: WindowMenuRole, + } +} + +// These roles are Mac only + +// AppMenu provides a MenuItem with the whole default "App" menu (About, Services, etc.) +func AppMenu() *MenuItem { + return &MenuItem{ + Role: AppMenuRole, + } +} + +/* +// Hide provides a MenuItem that maps to the hide action. +func Hide() *MenuItem { + return &MenuItem{ + Role: HideRole, + } +} + +// HideOthers provides a MenuItem that maps to the hideOtherApplications action. +func HideOthers() *MenuItem { + return &MenuItem{ + Role: HideOthersRole, + } +} + +// UnHide provides a MenuItem that maps to the unHideAllApplications action. +func UnHide() *MenuItem { + return &MenuItem{ + Role: UnhideRole, + } +} + +// Front provides a MenuItem that maps to the arrangeInFront action. +func Front() *MenuItem { + return &MenuItem{ + Role: FrontRole, + } +} + +// Zoom provides a MenuItem that maps to the performZoom action. +func Zoom() *MenuItem { + return &MenuItem{ + Role: ZoomRole, + } +} + +// WindowSubMenu provides a MenuItem with the "Window" submenu. +func WindowSubMenu() *MenuItem { + return &MenuItem{ + Role: WindowSubMenuRole, + } +} + +// HelpSubMenu provides a MenuItem with the "Help" submenu. +func HelpSubMenu() *MenuItem { + return &MenuItem{ + Role: HelpSubMenuRole, + } +} +*/ diff --git a/v2/pkg/menu/styledlabel.go b/v2/pkg/menu/styledlabel.go new file mode 100644 index 000000000..1e996b971 --- /dev/null +++ b/v2/pkg/menu/styledlabel.go @@ -0,0 +1,261 @@ +package menu + +import ( + "fmt" + "strconv" + "strings" +) + +type TextStyle int + +const ( + Bold TextStyle = 1 << 0 + Faint TextStyle = 1 << 1 + Italic TextStyle = 1 << 2 + Blinking TextStyle = 1 << 3 + Inversed TextStyle = 1 << 4 + Invisible TextStyle = 1 << 5 + Underlined TextStyle = 1 << 6 + Strikethrough TextStyle = 1 << 7 +) + +type StyledText struct { + Label string + FgCol *Col + BgCol *Col + Style TextStyle +} + +func (s *StyledText) Bold() bool { + return s.Style&Bold == Bold +} + +func (s *StyledText) Faint() bool { + return s.Style&Faint == Faint +} + +func (s *StyledText) Italic() bool { + return s.Style&Italic == Italic +} + +func (s *StyledText) Blinking() bool { + return s.Style&Blinking == Blinking +} + +func (s *StyledText) Inversed() bool { + return s.Style&Inversed == Inversed +} + +func (s *StyledText) Invisible() bool { + return s.Style&Invisible == Invisible +} + +func (s *StyledText) Underlined() bool { + return s.Style&Underlined == Underlined +} + +func (s *StyledText) Strikethrough() bool { + return s.Style&Strikethrough == Strikethrough +} + +var ansiColorMap = map[string]map[string]*Col{ + "Normal": { + "30": Cols[0], + "31": Cols[1], + "32": Cols[2], + "33": Cols[3], + "34": Cols[4], + "35": Cols[5], + "36": Cols[6], + "37": Cols[7], + }, + "Bold": { + "30": Cols[8], + "31": Cols[9], + "32": Cols[10], + "33": Cols[11], + "34": Cols[12], + "35": Cols[13], + "36": Cols[14], + "37": Cols[15], + }, + "Faint": { + "30": Cols[0], + "31": Cols[1], + "32": Cols[2], + "33": Cols[3], + "34": Cols[4], + "35": Cols[5], + "36": Cols[6], + "37": Cols[7], + }, +} + +func ParseANSI(input string) ([]*StyledText, error) { + var result []*StyledText + invalid := fmt.Errorf("invalid ansi string") + missingTerminator := fmt.Errorf("missing escape terminator 'm'") + invalidTrueColorSequence := fmt.Errorf("invalid TrueColor sequence") + invalid256ColSequence := fmt.Errorf("invalid 256 colour sequence") + index := 0 + var currentStyledText *StyledText = &StyledText{} + + if len(input) == 0 { + return nil, invalid + } + + for { + // Read all chars to next escape code + esc := strings.Index(input, "\033[") + + // If no more esc chars, save what's left and return + if esc == -1 { + text := input[index:] + if len(text) > 0 { + currentStyledText.Label = text + result = append(result, currentStyledText) + } + return result, nil + } + label := input[:esc] + if len(label) > 0 { + currentStyledText.Label = label + result = append(result, currentStyledText) + currentStyledText = &StyledText{ + Label: "", + FgCol: currentStyledText.FgCol, + BgCol: currentStyledText.BgCol, + Style: currentStyledText.Style, + } + } + input = input[esc:] + // skip + input = input[2:] + + // Read in params + endesc := strings.Index(input, "m") + if endesc == -1 { + return nil, missingTerminator + } + paramText := input[:endesc] + input = input[endesc+1:] + params := strings.Split(paramText, ";") + colourMap := ansiColorMap["Normal"] + skip := 0 + for index, param := range params { + if skip > 0 { + skip-- + continue + } + switch param { + case "0": + // Reset styles + if len(params) == 1 { + if len(currentStyledText.Label) > 0 { + result = append(result, currentStyledText) + currentStyledText = &StyledText{ + Label: "", + FgCol: currentStyledText.FgCol, + BgCol: currentStyledText.BgCol, + Style: currentStyledText.Style, + } + continue + } + } + currentStyledText.Style = 0 + currentStyledText.FgCol = nil + currentStyledText.BgCol = nil + case "1": + // Bold + colourMap = ansiColorMap["Bold"] + currentStyledText.Style |= Bold + case "2": + // Dim/Feint + colourMap = ansiColorMap["Faint"] + currentStyledText.Style |= Faint + case "3": + // Italic + currentStyledText.Style |= Italic + case "4": + // Underlined + currentStyledText.Style |= Underlined + case "5": + // Blinking + currentStyledText.Style |= Blinking + case "7": + // Inverse + currentStyledText.Style |= Inversed + case "8": + // Invisible + currentStyledText.Style |= Invisible + case "9": + // Strikethrough + currentStyledText.Style |= Strikethrough + case "30", "31", "32", "33", "34", "35", "36", "37": + currentStyledText.FgCol = colourMap[param] + case "40", "41", "42", "43", "44", "45", "46", "47": + currentStyledText.BgCol = colourMap[param] + case "38", "48": + if len(params)-index < 2 { + return nil, invalid + } + // 256 colours + if params[index+1] == "5" { + skip = 2 + colIndexText := params[index+2] + colIndex, err := strconv.Atoi(colIndexText) + if err != nil { + return nil, invalid256ColSequence + } + if colIndex < 0 || colIndex > 255 { + return nil, invalid256ColSequence + } + if param == "38" { + currentStyledText.FgCol = Cols[colIndex] + continue + } + currentStyledText.BgCol = Cols[colIndex] + continue + } + // we must have 4 params left + if len(params)-index < 4 { + return nil, invalidTrueColorSequence + } + if params[index+1] != "2" { + return nil, invalidTrueColorSequence + } + var r, g, b uint8 + ri, err := strconv.Atoi(params[index+2]) + if err != nil { + return nil, invalidTrueColorSequence + } + gi, err := strconv.Atoi(params[index+3]) + if err != nil { + return nil, invalidTrueColorSequence + } + bi, err := strconv.Atoi(params[index+4]) + if err != nil { + return nil, invalidTrueColorSequence + } + if bi > 255 || gi > 255 || ri > 255 { + return nil, invalidTrueColorSequence + } + if bi < 0 || gi < 0 || ri < 0 { + return nil, invalidTrueColorSequence + } + r = uint8(ri) + g = uint8(gi) + b = uint8(bi) + skip = 4 + colvalue := fmt.Sprintf("#%02x%02x%02x", r, g, b) + if param == "38" { + currentStyledText.FgCol = &Col{Hex: colvalue, Rgb: Rgb{r, g, b}} + continue + } + currentStyledText.BgCol = &Col{Hex: colvalue} + default: + return nil, invalid + } + } + } +} diff --git a/v2/pkg/menu/styledlabel_test.go b/v2/pkg/menu/styledlabel_test.go new file mode 100644 index 000000000..1c43480e8 --- /dev/null +++ b/v2/pkg/menu/styledlabel_test.go @@ -0,0 +1,197 @@ +package menu + +import ( + "testing" + + "github.com/matryer/is" +) + +func TestParseAnsi16SingleColour(t *testing.T) { + is := is.New(t) + tests := []struct { + name string + input string + wantText string + wantColor string + wantErr bool + }{ + {"No formatting", "Hello World", "Hello World", "", false}, + {"Black", "\u001b[0;30mHello World\033[0m", "Hello World", "Black", false}, + {"Red", "\u001b[0;31mHello World\033[0m", "Hello World", "Maroon", false}, + {"Green", "\u001b[0;32mHello World\033[0m", "Hello World", "Green", false}, + {"Yellow", "\u001b[0;33m😀\033[0m", "😀", "Olive", false}, + {"Blue", "\u001b[0;34m123\033[0m", "123", "Navy", false}, + {"Purple", "\u001b[0;35m👩🏽‍🔧\u001B[0m", "👩🏽‍🔧", "Purple", false}, + {"Cyan", "\033[0;36m😀\033[0m", "😀", "Teal", false}, + {"White", "\u001b[0;37m[0;37m\033[0m", "[0;37m", "Silver", false}, + {"Black Bold", "\u001b[1;30mHello World\033[0m", "Hello World", "Grey", false}, + {"Red Bold", "\u001b[1;31mHello World\033[0m", "Hello World", "Red", false}, + {"Green Bold", "\u001b[1;32mTest\033[0m", "Test", "Lime", false}, + {"Yellow Bold", "\u001b[1;33m😀\033[0m", "😀", "Yellow", false}, + {"Blue Bold", "\u001b[1;34m123\033[0m", "123", "Blue", false}, + {"Purple Bold", "\u001b[1;35m👩🏽‍🔧\u001B[0m", "👩🏽‍🔧", "Fuchsia", false}, + {"Cyan Bold", "\033[1;36m😀\033[0m", "😀", "Aqua", false}, + {"White Bold", "\u001b[1;37m[0;37m\033[0m", "[0;37m", "White", false}, + {"Blank", "", "", "", true}, + {"Emoji", "😀👩🏽‍🔧", "😀👩🏽‍🔧", "", false}, + {"Spaces", " ", " ", "", false}, + {"Bad code", "\u001b[1 ", "", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseANSI(tt.input) + is.Equal(err != nil, tt.wantErr) + expectedLength := 1 + if tt.wantErr { + expectedLength = 0 + } + is.Equal(len(got), expectedLength) + if expectedLength == 1 { + if len(tt.wantColor) > 0 { + is.True(got[0].FgCol != nil) + is.Equal(got[0].FgCol.Name, tt.wantColor) + } + } + }) + } +} + +func TestParseAnsi16MultiColour(t *testing.T) { + is := is.New(t) + tests := []struct { + name string + input string + want []*StyledText + wantErr bool + }{ + {"Black & Red", "\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{ + {Label: "Hello World", FgCol: &Col{Name: "Black"}}, + {Label: "Hello World", FgCol: &Col{Name: "Maroon"}}, + }, false}, + {"Text then Black & Red", "This is great!\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{ + {Label: "This is great!"}, + {Label: "Hello World", FgCol: &Col{Name: "Black"}}, + {Label: "Hello World", FgCol: &Col{Name: "Maroon"}}, + }, false}, + {"Text Reset then Black & Red", "This is great!\u001B[0m\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{ + {Label: "This is great!"}, + {Label: "Hello World", FgCol: &Col{Name: "Black"}}, + {Label: "Hello World", FgCol: &Col{Name: "Maroon"}}, + }, false}, + {"Black & Red no reset", "\u001B[0;30mHello World\u001B[0;31mHello World", []*StyledText{ + {Label: "Hello World", FgCol: &Col{Name: "Black"}}, + {Label: "Hello World", FgCol: &Col{Name: "Maroon"}}, + }, false}, + {"Black,space,Red", "\u001B[0;30mHello World\u001B[0m \u001B[0;31mHello World\u001B[0m", []*StyledText{ + {Label: "Hello World", FgCol: &Col{Name: "Black"}}, + {Label: " "}, + {Label: "Hello World", FgCol: &Col{Name: "Maroon"}}, + }, false}, + {"Black,Red,Blue,Green underlined", "\033[4;30mBlack\u001B[0m\u001B[4;31mRed\u001B[0m\u001B[4;34mBlue\u001B[0m\u001B[4;32mGreen\u001B[0m", []*StyledText{ + {Label: "Black", FgCol: &Col{Name: "Black"}, Style: Underlined}, + {Label: "Red", FgCol: &Col{Name: "Maroon"}, Style: Underlined}, + {Label: "Blue", FgCol: &Col{Name: "Navy"}, Style: Underlined}, + {Label: "Green", FgCol: &Col{Name: "Green"}, Style: Underlined}, + }, false}, + {"Black,Red,Blue,Green bold", "\033[1;30mBlack\u001B[0m\u001B[1;31mRed\u001B[0m\u001B[1;34mBlue\u001B[0m\u001B[1;32mGreen\u001B[0m", []*StyledText{ + {Label: "Black", FgCol: &Col{Name: "Grey"}, Style: Bold}, + {Label: "Red", FgCol: &Col{Name: "Red"}, Style: Bold}, + {Label: "Blue", FgCol: &Col{Name: "Blue"}, Style: Bold}, + {Label: "Green", FgCol: &Col{Name: "Lime"}, Style: Bold}, + }, false}, + {"Green Feint & Yellow Italic", "\u001B[2;32m👩🏽‍🔧\u001B[0m\u001B[0;3;33m👩🏽‍🔧\u001B[0m", []*StyledText{ + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Green"}, Style: Faint}, + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Olive"}, Style: Italic}, + }, false}, + {"Green Blinking & Yellow Inversed", "\u001B[5;32m👩🏽‍🔧\u001B[0m\u001B[0;7;33m👩🏽‍🔧\u001B[0m", []*StyledText{ + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Green"}, Style: Blinking}, + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Olive"}, Style: Inversed}, + }, false}, + {"Green Invisible & Yellow Invisible & Strikethrough", "\u001B[8;32m👩🏽‍🔧\u001B[0m\u001B[9;33m👩🏽‍🔧\u001B[0m", []*StyledText{ + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Green"}, Style: Invisible}, + {Label: "👩🏽‍🔧", FgCol: &Col{Name: "Olive"}, Style: Strikethrough}, + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseANSI(tt.input) + is.Equal(err != nil, tt.wantErr) + for index, w := range tt.want { + is.Equal(got[index].Label, w.Label) + if w.FgCol != nil { + is.Equal(got[index].FgCol.Name, w.FgCol.Name) + } + is.Equal(got[index].Style, w.Style) + } + }) + } +} + +func TestParseAnsi256(t *testing.T) { + is := is.New(t) + tests := []struct { + name string + input string + want []*StyledText + wantErr bool + }{ + {"Grey93 & DarkViolet", "\u001B[38;5;255mGrey93\u001B[0m\u001B[38;5;128mDarkViolet\u001B[0m", []*StyledText{ + {Label: "Grey93", FgCol: &Col{Name: "Grey93"}}, + {Label: "DarkViolet", FgCol: &Col{Name: "DarkViolet"}}, + }, false}, + {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;255mGrey93\u001B[0m\u001B[0;3;38;5;128mDarkViolet\u001B[0m", []*StyledText{ + {Label: "Grey93", FgCol: &Col{Name: "Grey93"}, Style: Bold}, + {Label: "DarkViolet", FgCol: &Col{Name: "DarkViolet"}, Style: Italic}, + }, false}, + {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;256mGrey93\u001B[0m", nil, true}, + {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;-1mGrey93\u001B[0m", nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseANSI(tt.input) + is.Equal(err != nil, tt.wantErr) + for index, w := range tt.want { + is.Equal(got[index].Label, w.Label) + if w.FgCol != nil { + is.Equal(got[index].FgCol.Name, w.FgCol.Name) + } + is.Equal(got[index].Style, w.Style) + } + }) + } +} + +func TestParseAnsiTrueColor(t *testing.T) { + is := is.New(t) + tests := []struct { + name string + input string + want []*StyledText + wantErr bool + }{ + {"Red", "\u001B[38;2;255;0;0mRed\u001B[0m", []*StyledText{ + {Label: "Red", FgCol: &Col{Rgb: Rgb{255, 0, 0}, Hex: "#ff0000"}}, + }, false}, + {"Red, text, Green", "\u001B[38;2;255;0;0mRed\u001B[0mI am plain text\u001B[38;2;0;255;0mGreen\u001B[0m", []*StyledText{ + {Label: "Red", FgCol: &Col{Rgb: Rgb{255, 0, 0}, Hex: "#ff0000"}}, + {Label: "I am plain text"}, + {Label: "Green", FgCol: &Col{Rgb: Rgb{0, 255, 0}, Hex: "#00ff00"}}, + }, false}, + {"Bad 1", "\u001B[38;2;256;0;0mRed\u001B[0m", nil, true}, + {"Bad 2", "\u001B[38;2;-1;0;0mRed\u001B[0m", nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseANSI(tt.input) + is.Equal(err != nil, tt.wantErr) + for index, w := range tt.want { + is.Equal(got[index].Label, w.Label) + if w.FgCol != nil { + is.Equal(got[index].FgCol.Hex, w.FgCol.Hex) + is.Equal(got[index].FgCol.Rgb, w.FgCol.Rgb) + } + is.Equal(got[index].Style, w.Style) + } + }) + } +} diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go new file mode 100644 index 000000000..c8728f1f7 --- /dev/null +++ b/v2/pkg/menu/tray.go @@ -0,0 +1,42 @@ +package menu + +// TrayMenu are the options +type TrayMenu struct { + // Label is the text we wish to display in the tray + Label string + + // Image is the name of the tray icon we wish to display. + // These are read up during build from /trayicons and + // the filenames are used as IDs, minus the extension + // EG: /trayicons/main.png can be referenced here with "main" + // If the image is not a filename, it will be treated as base64 image data + Image string + + // MacTemplateImage indicates that on a Mac, this image is a template image + MacTemplateImage bool + + // Text Colour + RGBA string + + // Font + FontSize int + FontName string + + // Tooltip + Tooltip string + + // Callback function when menu clicked + // Click Callback `json:"-"` + + // Disabled makes the item unselectable + Disabled bool + + // Menu is the initial menu we wish to use for the tray + Menu *Menu + + // OnOpen is called when the Menu is opened + OnOpen func() + + // OnClose is called when the Menu is closed + OnClose func() +} diff --git a/v2/pkg/menu/type.go b/v2/pkg/menu/type.go new file mode 100644 index 000000000..829ea05ad --- /dev/null +++ b/v2/pkg/menu/type.go @@ -0,0 +1,17 @@ +package menu + +// Type of the menu item +type Type string + +const ( + // TextType is the text menuitem type + TextType Type = "Text" + // SeparatorType is the Separator menuitem type + SeparatorType Type = "Separator" + // SubmenuType is the Submenu menuitem type + SubmenuType Type = "Submenu" + // CheckboxType is the Checkbox menuitem type + CheckboxType Type = "Checkbox" + // RadioType is the Radio menuitem type + RadioType Type = "Radio" +) diff --git a/v2/pkg/menu/windows.go b/v2/pkg/menu/windows.go new file mode 100644 index 000000000..d251e287e --- /dev/null +++ b/v2/pkg/menu/windows.go @@ -0,0 +1,13 @@ +package menu + +/* +// DefaultWindowsMenu returns a default menu including the default +// Application and Edit menus. Use `.Append()` to add to it. +func DefaultWindowsMenu() *Menu { + return NewMenuFromItems( + FileMenu(), + EditMenu(), + WindowMenu(), + ) +} +*/ diff --git a/v2/pkg/options/assetserver/middleware.go b/v2/pkg/options/assetserver/middleware.go new file mode 100644 index 000000000..b3826ab7d --- /dev/null +++ b/v2/pkg/options/assetserver/middleware.go @@ -0,0 +1,20 @@ +package assetserver + +import ( + "net/http" +) + +// Middleware defines a HTTP middleware that can be applied to the AssetServer. +// The handler passed as next is the next handler in the chain. One can decide to call the next handler +// or implement a specialized handling. +type Middleware func(next http.Handler) http.Handler + +// ChainMiddleware allows chaining multiple middlewares to one middleware. +func ChainMiddleware(middleware ...Middleware) Middleware { + return func(h http.Handler) http.Handler { + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h + } +} diff --git a/v2/pkg/options/assetserver/options.go b/v2/pkg/options/assetserver/options.go new file mode 100644 index 000000000..674451a0c --- /dev/null +++ b/v2/pkg/options/assetserver/options.go @@ -0,0 +1,45 @@ +package assetserver + +import ( + "fmt" + "io/fs" + "net/http" +) + +// Options defines the configuration of the AssetServer. +type Options struct { + // Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns + // `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET + // request from it. + // + // If set to nil, all GET requests will be forwarded to Handler. + Assets fs.FS + + // Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all + // non GET requests will always be served from this Handler. + // + // If not defined, the result is the following in cases where the Handler would have been called: + // GET request: `http.StatusNotFound` + // Other request: `http.StatusMethodNotAllowed` + Handler http.Handler + + // Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default + // request handler dynamically, e.g. implement specialized Routing etc. + // The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default + // handler used by the AssetServer as an argument. + // + // If not defined, the default AssetServer request chain is executed. + // + // Multiple Middlewares can be chained together with: + // ChainMiddleware(middleware ...Middleware) Middleware + Middleware Middleware +} + +// Validate the options +func (o Options) Validate() error { + if o.Assets == nil && o.Handler == nil && o.Middleware == nil { + return fmt.Errorf("AssetServer options invalid: either Assets, Handler or Middleware must be set") + } + + return nil +} diff --git a/v2/pkg/options/debug.go b/v2/pkg/options/debug.go new file mode 100644 index 000000000..b1e989b9a --- /dev/null +++ b/v2/pkg/options/debug.go @@ -0,0 +1,7 @@ +package options + +// Debug options which are taken into account in debug builds. +type Debug struct { + // OpenInspectorOnStartup opens the inspector on startup of the app. + OpenInspectorOnStartup bool +} diff --git a/v2/pkg/options/linux/linux.go b/v2/pkg/options/linux/linux.go new file mode 100644 index 000000000..797450c27 --- /dev/null +++ b/v2/pkg/options/linux/linux.go @@ -0,0 +1,56 @@ +package linux + +// WebviewGpuPolicy values used for determining the webview's hardware acceleration policy. +type WebviewGpuPolicy int + +const ( + // WebviewGpuPolicyAlways Hardware acceleration is always enabled. + WebviewGpuPolicyAlways WebviewGpuPolicy = iota + // WebviewGpuPolicyOnDemand Hardware acceleration is enabled/disabled as request by web contents. + WebviewGpuPolicyOnDemand + // WebviewGpuPolicyNever Hardware acceleration is always disabled. + WebviewGpuPolicyNever +) + +// Options specific to Linux builds +type Options struct { + // Icon Sets up the icon representing the window. This icon is used when the window is minimized + // (also known as iconified). + Icon []byte + + // WindowIsTranslucent sets the window's background to transparent when enabled. + WindowIsTranslucent bool + + // Messages are messages that can be customised + Messages *Messages + + // WebviewGpuPolicy used for determining the hardware acceleration policy for the webview. + // - WebviewGpuPolicyAlways + // - WebviewGpuPolicyOnDemand + // - WebviewGpuPolicyNever + // + // Due to https://github.com/wailsapp/wails/issues/2977, if options.Linux is nil + // in the call to wails.Run(), WebviewGpuPolicy is set by default to WebviewGpuPolicyNever. + // Client code may override this behavior by passing a non-nil Options and set + // WebviewGpuPolicy as needed. + WebviewGpuPolicy WebviewGpuPolicy + + // ProgramName is used to set the program's name for the window manager via GTK's g_set_prgname(). + //This name should not be localized. [see the docs] + // + //When a .desktop file is created this value helps with window grouping and desktop icons when the .desktop file's Name + //property differs form the executable's filename. + // + //[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html + ProgramName string +} + +type Messages struct { + WebKit2GTKMinRequired string +} + +func DefaultMessages() *Messages { + return &Messages{ + WebKit2GTKMinRequired: "This application requires at least WebKit2GTK %s to be installed.", + } +} diff --git a/v2/pkg/options/mac/appearance.go b/v2/pkg/options/mac/appearance.go new file mode 100644 index 000000000..934bb208a --- /dev/null +++ b/v2/pkg/options/mac/appearance.go @@ -0,0 +1,23 @@ +package mac + +// AppearanceType is a type of Appearance for Cocoa windows +type AppearanceType string + +const ( + // DefaultAppearance uses the default system value + DefaultAppearance AppearanceType = "" + // NSAppearanceNameAqua - The standard light system appearance. + NSAppearanceNameAqua AppearanceType = "NSAppearanceNameAqua" + // NSAppearanceNameDarkAqua - The standard dark system appearance. + NSAppearanceNameDarkAqua AppearanceType = "NSAppearanceNameDarkAqua" + // NSAppearanceNameVibrantLight - The light vibrant appearance + NSAppearanceNameVibrantLight AppearanceType = "NSAppearanceNameVibrantLight" + // NSAppearanceNameAccessibilityHighContrastAqua - A high-contrast version of the standard light system appearance. + NSAppearanceNameAccessibilityHighContrastAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastAqua" + // NSAppearanceNameAccessibilityHighContrastDarkAqua - A high-contrast version of the standard dark system appearance. + NSAppearanceNameAccessibilityHighContrastDarkAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastDarkAqua" + // NSAppearanceNameAccessibilityHighContrastVibrantLight - A high-contrast version of the light vibrant appearance. + NSAppearanceNameAccessibilityHighContrastVibrantLight AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantLight" + // NSAppearanceNameAccessibilityHighContrastVibrantDark - A high-contrast version of the dark vibrant appearance. + NSAppearanceNameAccessibilityHighContrastVibrantDark AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantDark" +) diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go new file mode 100644 index 000000000..152145114 --- /dev/null +++ b/v2/pkg/options/mac/mac.go @@ -0,0 +1,31 @@ +package mac + +//type ActivationPolicy int +// +//const ( +// NSApplicationActivationPolicyRegular ActivationPolicy = 0 +// NSApplicationActivationPolicyAccessory ActivationPolicy = 1 +// NSApplicationActivationPolicyProhibited ActivationPolicy = 2 +//) + +type AboutInfo struct { + Title string + Message string + Icon []byte +} + +// Options are options specific to Mac +type Options struct { + TitleBar *TitleBar + Appearance AppearanceType + ContentProtection bool + WebviewIsTransparent bool + WindowIsTranslucent bool + Preferences *Preferences + DisableZoom bool + // ActivationPolicy ActivationPolicy + About *AboutInfo + OnFileOpen func(filePath string) `json:"-"` + OnUrlOpen func(filePath string) `json:"-"` + // URLHandlers map[string]func(string) +} diff --git a/v2/pkg/options/mac/preferences.go b/v2/pkg/options/mac/preferences.go new file mode 100644 index 000000000..0749ccb18 --- /dev/null +++ b/v2/pkg/options/mac/preferences.go @@ -0,0 +1,21 @@ +package mac + +import "github.com/leaanthony/u" + +var ( + Enabled = u.True + Disabled = u.False +) + +// Preferences allows to set webkit preferences +type Preferences struct { + // A Boolean value that indicates whether pressing the tab key changes the focus to links and form controls. + // Set to false by default. + TabFocusesLinks u.Bool + // A Boolean value that indicates whether to allow people to select or otherwise interact with text. + // Set to true by default. + TextInteractionEnabled u.Bool + // A Boolean value that indicates whether a web view can display content full screen. + // Set to false by default + FullscreenEnabled u.Bool +} diff --git a/v2/pkg/options/mac/titlebar.go b/v2/pkg/options/mac/titlebar.go new file mode 100644 index 000000000..51e0832ca --- /dev/null +++ b/v2/pkg/options/mac/titlebar.go @@ -0,0 +1,52 @@ +package mac + +// TitleBar contains options for the Mac titlebar +type TitleBar struct { + TitlebarAppearsTransparent bool + HideTitle bool + HideTitleBar bool + FullSizeContent bool + UseToolbar bool + HideToolbarSeparator bool +} + +// TitleBarDefault results in the default Mac Titlebar +func TitleBarDefault() *TitleBar { + return &TitleBar{ + TitlebarAppearsTransparent: false, + HideTitle: false, + HideTitleBar: false, + FullSizeContent: false, + UseToolbar: false, + HideToolbarSeparator: false, + } +} + +// Credit: Comments from Electron site + +// TitleBarHidden results in a hidden title bar and a full size content window, +// yet the title bar still has the standard window controls (“traffic lights”) +// in the top left. +func TitleBarHidden() *TitleBar { + return &TitleBar{ + TitlebarAppearsTransparent: true, + HideTitle: true, + HideTitleBar: false, + FullSizeContent: true, + UseToolbar: false, + HideToolbarSeparator: false, + } +} + +// TitleBarHiddenInset results in a hidden title bar with an alternative look where +// the traffic light buttons are slightly more inset from the window edge. +func TitleBarHiddenInset() *TitleBar { + return &TitleBar{ + TitlebarAppearsTransparent: true, + HideTitle: true, + HideTitleBar: false, + FullSizeContent: true, + UseToolbar: true, + HideToolbarSeparator: true, + } +} diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go new file mode 100644 index 000000000..0f62d5e4b --- /dev/null +++ b/v2/pkg/options/options.go @@ -0,0 +1,276 @@ +package options + +import ( + "context" + "html" + "io/fs" + "net/http" + "os" + "path/filepath" + "runtime" + + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/linux" + "github.com/wailsapp/wails/v2/pkg/options/mac" + "github.com/wailsapp/wails/v2/pkg/options/windows" + + "github.com/wailsapp/wails/v2/pkg/menu" + + "github.com/wailsapp/wails/v2/pkg/logger" +) + +type WindowStartState int + +const ( + Normal WindowStartState = 0 + Maximised WindowStartState = 1 + Minimised WindowStartState = 2 + Fullscreen WindowStartState = 3 +) + +type Experimental struct{} + +// App contains options for creating the App +type App struct { + Title string + Width int + Height int + DisableResize bool + Fullscreen bool + Frameless bool + MinWidth int + MinHeight int + MaxWidth int + MaxHeight int + StartHidden bool + HideWindowOnClose bool + AlwaysOnTop bool + // BackgroundColour is the background colour of the window + // You can use the options.NewRGB and options.NewRGBA functions to create a new colour + BackgroundColour *RGBA + // Deprecated: Use AssetServer.Assets instead. + Assets fs.FS + // Deprecated: Use AssetServer.Handler instead. + AssetsHandler http.Handler + // AssetServer configures the Assets for the application + AssetServer *assetserver.Options + Menu *menu.Menu + Logger logger.Logger `json:"-"` + LogLevel logger.LogLevel + LogLevelProduction logger.LogLevel + OnStartup func(ctx context.Context) `json:"-"` + OnDomReady func(ctx context.Context) `json:"-"` + OnShutdown func(ctx context.Context) `json:"-"` + OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"` + Bind []interface{} + EnumBind []interface{} + WindowStartState WindowStartState + + // ErrorFormatter overrides the formatting of errors returned by backend methods + ErrorFormatter ErrorFormatter + + // CSS property to test for draggable elements. Default "--wails-draggable" + CSSDragProperty string + + // The CSS Value that the CSSDragProperty must have to be draggable, EG: "drag" + CSSDragValue string + + // EnableDefaultContextMenu enables the browser's default context-menu in production + // This menu is already enabled in development and debug builds + EnableDefaultContextMenu bool + + // EnableFraudulentWebsiteDetection enables scan services for fraudulent content, such as malware or phishing attempts. + // These services might send information from your app like URLs navigated to and possibly other content to cloud + // services of Apple and Microsoft. + EnableFraudulentWebsiteDetection bool + + SingleInstanceLock *SingleInstanceLock + + Windows *windows.Options + Mac *mac.Options + Linux *linux.Options + + // Experimental options + Experimental *Experimental + + // Debug options for debug builds. These options will be ignored in a production build. + Debug Debug + + // DragAndDrop options for drag and drop behavior + DragAndDrop *DragAndDrop + + // DisablePanicRecovery disables the panic recovery system in messages processing + DisablePanicRecovery bool + + // List of additional allowed origins for bindings in format "https://*.myapp.com,https://example.com" + BindingsAllowedOrigins string +} + +type ErrorFormatter func(error) any + +type RGBA struct { + R uint8 `json:"r"` + G uint8 `json:"g"` + B uint8 `json:"b"` + A uint8 `json:"a"` +} + +// NewRGBA creates a new RGBA struct with the given values +func NewRGBA(r, g, b, a uint8) *RGBA { + return &RGBA{ + R: r, + G: g, + B: b, + A: a, + } +} + +// NewRGB creates a new RGBA struct with the given values and Alpha set to 255 +func NewRGB(r, g, b uint8) *RGBA { + return &RGBA{ + R: r, + G: g, + B: b, + A: 255, + } +} + +// MergeDefaults will set the minimum default values for an application +func MergeDefaults(appoptions *App) { + // Do set defaults + if appoptions.Width <= 0 { + appoptions.Width = 1024 + } + if appoptions.Height <= 0 { + appoptions.Height = 768 + } + if appoptions.Logger == nil { + appoptions.Logger = logger.NewDefaultLogger() + } + if appoptions.LogLevel == 0 { + appoptions.LogLevel = logger.INFO + } + if appoptions.LogLevelProduction == 0 { + appoptions.LogLevelProduction = logger.ERROR + } + if appoptions.CSSDragProperty == "" { + appoptions.CSSDragProperty = "--wails-draggable" + } + if appoptions.CSSDragValue == "" { + appoptions.CSSDragValue = "drag" + } + if appoptions.DragAndDrop == nil { + appoptions.DragAndDrop = &DragAndDrop{} + } + if appoptions.DragAndDrop.CSSDropProperty == "" { + appoptions.DragAndDrop.CSSDropProperty = "--wails-drop-target" + } + if appoptions.DragAndDrop.CSSDropValue == "" { + appoptions.DragAndDrop.CSSDropValue = "drop" + } + if appoptions.BackgroundColour == nil { + appoptions.BackgroundColour = &RGBA{ + R: 255, + G: 255, + B: 255, + A: 255, + } + } + + // Ensure max and min are valid + processMinMaxConstraints(appoptions) + + // Default menus + processMenus(appoptions) + + // Process Drag Options + processDragOptions(appoptions) +} + +type SingleInstanceLock struct { + // uniqueId that will be used for setting up messaging between instances + UniqueId string + OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData) +} + +type SecondInstanceData struct { + Args []string + WorkingDirectory string +} + +type DragAndDrop struct { + + // EnableFileDrop enables wails' drag and drop functionality that returns the dropped in files' absolute paths. + EnableFileDrop bool + + // Disable webview's drag and drop functionality. + // + // It can be used to prevent accidental file opening of dragged in files in the webview, when there is no need for drag and drop. + DisableWebViewDrop bool + + // CSS property to test for drag and drop target elements. Default "--wails-drop-target" + CSSDropProperty string + + // The CSS Value that the CSSDropProperty must have to be a valid drop target. Default "drop" + CSSDropValue string +} + +func NewSecondInstanceData() (*SecondInstanceData, error) { + ex, err := os.Executable() + if err != nil { + return nil, err + } + workingDirectory := filepath.Dir(ex) + + return &SecondInstanceData{ + Args: os.Args[1:], + WorkingDirectory: workingDirectory, + }, nil +} + +func processMenus(appoptions *App) { + switch runtime.GOOS { + case "darwin": + if appoptions.Menu == nil { + items := []*menu.MenuItem{ + menu.EditMenu(), + } + if !appoptions.Frameless { + items = append(items, menu.WindowMenu()) // Current options in Window Menu only work if not frameless + } + + appoptions.Menu = menu.NewMenuFromItems(menu.AppMenu(), items...) + } + } +} + +func processMinMaxConstraints(appoptions *App) { + if appoptions.MinWidth > 0 && appoptions.MaxWidth > 0 { + if appoptions.MinWidth > appoptions.MaxWidth { + appoptions.MinWidth = appoptions.MaxWidth + } + } + if appoptions.MinHeight > 0 && appoptions.MaxHeight > 0 { + if appoptions.MinHeight > appoptions.MaxHeight { + appoptions.MinHeight = appoptions.MaxHeight + } + } + // Ensure width and height are limited if max/min is set + if appoptions.Width < appoptions.MinWidth { + appoptions.Width = appoptions.MinWidth + } + if appoptions.MaxWidth > 0 && appoptions.Width > appoptions.MaxWidth { + appoptions.Width = appoptions.MaxWidth + } + if appoptions.Height < appoptions.MinHeight { + appoptions.Height = appoptions.MinHeight + } + if appoptions.MaxHeight > 0 && appoptions.Height > appoptions.MaxHeight { + appoptions.Height = appoptions.MaxHeight + } +} + +func processDragOptions(appoptions *App) { + appoptions.CSSDragProperty = html.EscapeString(appoptions.CSSDragProperty) + appoptions.CSSDragValue = html.EscapeString(appoptions.CSSDragValue) +} diff --git a/v2/pkg/options/options_test.go b/v2/pkg/options/options_test.go new file mode 100644 index 000000000..0cacd702d --- /dev/null +++ b/v2/pkg/options/options_test.go @@ -0,0 +1,85 @@ +package options + +import ( + "testing" +) + +func TestMergeDefaultsWH(t *testing.T) { + tests := []struct { + name string + appoptions *App + wantWidth int + wantHeight int + }{ + { + name: "No width and height", + appoptions: &App{}, + wantWidth: 1024, + wantHeight: 768, + }, + { + name: "Basic width and height", + appoptions: &App{ + Width: 800, + Height: 600, + }, + wantWidth: 800, + wantHeight: 600, + }, + { + name: "With MinWidth and MinHeight", + appoptions: &App{ + Width: 200, + MinWidth: 800, + Height: 100, + MinHeight: 600, + }, + wantWidth: 800, + wantHeight: 600, + }, + { + name: "With MaxWidth and MaxHeight", + appoptions: &App{ + Width: 900, + MaxWidth: 800, + Height: 700, + MaxHeight: 600, + }, + wantWidth: 800, + wantHeight: 600, + }, + { + name: "With MinWidth more than MaxWidth", + appoptions: &App{ + Width: 900, + MinWidth: 900, + MaxWidth: 800, + Height: 600, + }, + wantWidth: 800, + wantHeight: 600, + }, + { + name: "With MinHeight more than MaxHeight", + appoptions: &App{ + Width: 800, + Height: 700, + MinHeight: 900, + MaxHeight: 600, + }, + wantWidth: 800, + wantHeight: 600, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + MergeDefaults(tt.appoptions) + if tt.appoptions.Width != tt.wantWidth { + t.Errorf("MergeDefaults().Width =%v, want %v", tt.appoptions.Width, tt.wantWidth) + } + if tt.appoptions.Height != tt.wantHeight { + t.Errorf("MergeDefaults().Height =%v, want %v", tt.appoptions.Height, tt.wantHeight) + } + }) + } +} diff --git a/v2/pkg/options/windows/windows.go b/v2/pkg/options/windows/windows.go new file mode 100644 index 000000000..1fe351455 --- /dev/null +++ b/v2/pkg/options/windows/windows.go @@ -0,0 +1,167 @@ +package windows + +type Theme int + +type Messages struct { + InstallationRequired string + UpdateRequired string + MissingRequirements string + Webview2NotInstalled string + Error string + FailedToInstall string + DownloadPage string + PressOKToInstall string + ContactAdmin string + InvalidFixedWebview2 string + WebView2ProcessCrash string +} + +const ( + // SystemDefault will use whatever the system theme is. The application will follow system theme changes. + SystemDefault Theme = 0 + // Dark Mode + Dark Theme = 1 + // Light Mode + Light Theme = 2 +) + +type BackdropType int32 + +const ( + Auto BackdropType = 0 + None BackdropType = 1 + Mica BackdropType = 2 + Acrylic BackdropType = 3 + Tabbed BackdropType = 4 +) + +const ( + // Default is 0, which means no changes to the default Windows DLL search behavior + DLLSearchDefault uint32 = 0 + // LoadLibrary flags for determining from where to search for a DLL + DLLSearchDontResolveDllReferences uint32 = 0x1 // windows.DONT_RESOLVE_DLL_REFERENCES + DLLSearchAsDataFile uint32 = 0x2 // windows.LOAD_LIBRARY_AS_DATAFILE + DLLSearchWithAlteredPath uint32 = 0x8 // windows.LOAD_WITH_ALTERED_SEARCH_PATH + DLLSearchIgnoreCodeAuthzLevel uint32 = 0x10 // windows.LOAD_IGNORE_CODE_AUTHZ_LEVEL + DLLSearchAsImageResource uint32 = 0x20 // windows.LOAD_LIBRARY_AS_IMAGE_RESOURCE + DLLSearchAsDataFileExclusive uint32 = 0x40 // windows.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE + DLLSearchRequireSignedTarget uint32 = 0x80 // windows.LOAD_LIBRARY_REQUIRE_SIGNED_TARGET + DLLSearchDllLoadDir uint32 = 0x100 // windows.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + DLLSearchApplicationDir uint32 = 0x200 // windows.LOAD_LIBRARY_SEARCH_APPLICATION_DIR + DLLSearchUserDirs uint32 = 0x400 // windows.LOAD_LIBRARY_SEARCH_USER_DIRS + DLLSearchSystem32 uint32 = 0x800 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32 + DLLSearchDefaultDirs uint32 = 0x1000 // windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS + DLLSearchSafeCurrentDirs uint32 = 0x2000 // windows.LOAD_LIBRARY_SAFE_CURRENT_DIRS + DLLSearchSystem32NoForwarder uint32 = 0x4000 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER + DLLSearchOsIntegrityContinuity uint32 = 0x8000 // windows.LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY +) + +func RGB(r, g, b uint8) int32 { + col := int32(b) + col = col<<8 | int32(g) + col = col<<8 | int32(r) + return col +} + +// ThemeSettings contains optional colours to use. +// They may be set using the hex values: 0x00BBGGRR +type ThemeSettings struct { + DarkModeTitleBar int32 + DarkModeTitleBarInactive int32 + DarkModeTitleText int32 + DarkModeTitleTextInactive int32 + DarkModeBorder int32 + DarkModeBorderInactive int32 + LightModeTitleBar int32 + LightModeTitleBarInactive int32 + LightModeTitleText int32 + LightModeTitleTextInactive int32 + LightModeBorder int32 + LightModeBorderInactive int32 +} + +// Options are options specific to Windows +type Options struct { + ContentProtection bool + WebviewIsTransparent bool + WindowIsTranslucent bool + DisableWindowIcon bool + + IsZoomControlEnabled bool + ZoomFactor float64 + + DisablePinchZoom bool + + // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. + // "Rounded Corners" are only available on Windows 11. + DisableFramelessWindowDecorations bool + + // Path where the WebView2 stores the user data. If empty %APPDATA%\[BinaryName.exe] will be used. + // If the path is not valid, a messagebox will be displayed with the error and the app will exit with error code. + WebviewUserDataPath string + + // Path to the directory with WebView2 executables. If empty WebView2 installed in the system will be used. + WebviewBrowserPath string + + // Dark/Light or System Default Theme + Theme Theme + + // Custom settings for dark/light mode + CustomTheme *ThemeSettings + + // Select the type of translucent backdrop. Requires Windows 11 22621 or later. + BackdropType BackdropType + + // User messages that can be customised + Messages *Messages + + // ResizeDebounceMS is the amount of time to debounce redraws of webview2 + // when resizing the window + ResizeDebounceMS uint16 + + // OnSuspend is called when Windows enters low power mode + OnSuspend func() + + // OnResume is called when Windows resumes from low power mode + OnResume func() + + // WebviewGpuIsDisabled is used to enable / disable GPU acceleration for the webview + WebviewGpuIsDisabled bool + + // WebviewDisableRendererCodeIntegrity disables the `RendererCodeIntegrity` of WebView2. Some Security Endpoint + // Protection Software inject themself into the WebView2 with unsigned or wrongly signed dlls, which is not allowed + // and will stop the WebView2 processes. Those security software need an update to fix this issue or one can disable + // the integrity check with this flag. + // + // The event viewer log contains `Code Integrity Errors` like mentioned here: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2051 + // + // !! Please keep in mind when disabling this feature, this also allows malicious software to inject into the WebView2 !! + WebviewDisableRendererCodeIntegrity bool + + // Configure whether swipe gestures should be enabled + EnableSwipeGestures bool + + // Class name for the window. If empty, 'wailsWindow' will be used. + WindowClassName string + + // DLLSearchPaths controls which directories are searched when loading DLLs + // Set to 0 for default behavior, or combine multiple flags with bitwise OR + // Example: DLLSearchApplicationDir | DLLSearchSystem32 + DLLSearchPaths uint32 +} + +func DefaultMessages() *Messages { + return &Messages{ + InstallationRequired: "The WebView2 runtime is required. Press Ok to download and install. Note: The installer will download silently so please wait.", + UpdateRequired: "The WebView2 runtime needs updating. Press Ok to download and install. Note: The installer will download silently so please wait.", + MissingRequirements: "Missing Requirements", + Webview2NotInstalled: "WebView2 runtime not installed", + Error: "Error", + FailedToInstall: "The runtime failed to install correctly. Please try again.", + DownloadPage: "This application requires the WebView2 runtime. Press OK to open the download page. Minimum version required: ", + PressOKToInstall: "Press Ok to install.", + ContactAdmin: "The WebView2 runtime is required to run this application. Please contact your system administrator.", + InvalidFixedWebview2: "The WebView2 runtime is manually specified, but It is not valid. Check minimum required version and webview2 path.", + WebView2ProcessCrash: "The WebView2 process crashed and the application needs to be restarted.", + } +} diff --git a/v2/pkg/runtime/browser.go b/v2/pkg/runtime/browser.go new file mode 100644 index 000000000..28ffc5e05 --- /dev/null +++ b/v2/pkg/runtime/browser.go @@ -0,0 +1,11 @@ +package runtime + +import ( + "context" +) + +// BrowserOpenURL uses the system default browser to open the url +func BrowserOpenURL(ctx context.Context, url string) { + appFrontend := getFrontend(ctx) + appFrontend.BrowserOpenURL(url) +} diff --git a/v2/pkg/runtime/clipboard.go b/v2/pkg/runtime/clipboard.go new file mode 100644 index 000000000..fab9e9e54 --- /dev/null +++ b/v2/pkg/runtime/clipboard.go @@ -0,0 +1,13 @@ +package runtime + +import "context" + +func ClipboardGetText(ctx context.Context) (string, error) { + appFrontend := getFrontend(ctx) + return appFrontend.ClipboardGetText() +} + +func ClipboardSetText(ctx context.Context, text string) error { + appFrontend := getFrontend(ctx) + return appFrontend.ClipboardSetText(text) +} diff --git a/v2/pkg/runtime/dialog.go b/v2/pkg/runtime/dialog.go new file mode 100644 index 000000000..16ae659e1 --- /dev/null +++ b/v2/pkg/runtime/dialog.go @@ -0,0 +1,80 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/fs" +) + +// FileFilter defines a filter for dialog boxes +type FileFilter = frontend.FileFilter + +// OpenDialogOptions contains the options for the OpenDialogOptions runtime method +type OpenDialogOptions = frontend.OpenDialogOptions + +// SaveDialogOptions contains the options for the SaveDialog runtime method +type SaveDialogOptions = frontend.SaveDialogOptions + +type DialogType = frontend.DialogType + +const ( + InfoDialog = frontend.InfoDialog + WarningDialog = frontend.WarningDialog + ErrorDialog = frontend.ErrorDialog + QuestionDialog = frontend.QuestionDialog +) + +// MessageDialogOptions contains the options for the Message dialogs, EG Info, Warning, etc runtime methods +type MessageDialogOptions = frontend.MessageDialogOptions + +// OpenDirectoryDialog prompts the user to select a directory +func OpenDirectoryDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) { + appFrontend := getFrontend(ctx) + if dialogOptions.DefaultDirectory != "" { + if !fs.DirExists(dialogOptions.DefaultDirectory) { + return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory) + } + } + return appFrontend.OpenDirectoryDialog(dialogOptions) +} + +// OpenFileDialog prompts the user to select a file +func OpenFileDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) { + appFrontend := getFrontend(ctx) + if dialogOptions.DefaultDirectory != "" { + if !fs.DirExists(dialogOptions.DefaultDirectory) { + return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory) + } + } + return appFrontend.OpenFileDialog(dialogOptions) +} + +// OpenMultipleFilesDialog prompts the user to select a file +func OpenMultipleFilesDialog(ctx context.Context, dialogOptions OpenDialogOptions) ([]string, error) { + appFrontend := getFrontend(ctx) + if dialogOptions.DefaultDirectory != "" { + if !fs.DirExists(dialogOptions.DefaultDirectory) { + return nil, fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory) + } + } + return appFrontend.OpenMultipleFilesDialog(dialogOptions) +} + +// SaveFileDialog prompts the user to select a file +func SaveFileDialog(ctx context.Context, dialogOptions SaveDialogOptions) (string, error) { + appFrontend := getFrontend(ctx) + if dialogOptions.DefaultDirectory != "" { + if !fs.DirExists(dialogOptions.DefaultDirectory) { + return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory) + } + } + return appFrontend.SaveFileDialog(dialogOptions) +} + +// MessageDialog show a message dialog to the user +func MessageDialog(ctx context.Context, dialogOptions MessageDialogOptions) (string, error) { + appFrontend := getFrontend(ctx) + return appFrontend.MessageDialog(dialogOptions) +} diff --git a/v2/pkg/runtime/draganddrop.go b/v2/pkg/runtime/draganddrop.go new file mode 100644 index 000000000..2db9c773c --- /dev/null +++ b/v2/pkg/runtime/draganddrop.go @@ -0,0 +1,37 @@ +package runtime + +import ( + "context" + "fmt" +) + +// OnFileDrop returns a slice of file path strings when a drop is finished. +func OnFileDrop(ctx context.Context, callback func(x, y int, paths []string)) { + if callback == nil { + LogError(ctx, "OnFileDrop called with a nil callback") + return + } + EventsOn(ctx, "wails:file-drop", func(optionalData ...interface{}) { + if len(optionalData) != 3 { + callback(0, 0, nil) + } + x, ok := optionalData[0].(int) + if !ok { + LogError(ctx, fmt.Sprintf("invalid x coordinate in drag and drop: %v", optionalData[0])) + } + y, ok := optionalData[1].(int) + if !ok { + LogError(ctx, fmt.Sprintf("invalid y coordinate in drag and drop: %v", optionalData[1])) + } + paths, ok := optionalData[2].([]string) + if !ok { + LogError(ctx, fmt.Sprintf("invalid path data in drag and drop: %v", optionalData[2])) + } + callback(x, y, paths) + }) +} + +// OnFileDropOff removes the drag and drop listeners and handlers. +func OnFileDropOff(ctx context.Context) { + EventsOff(ctx, "wails:file-drop") +} diff --git a/v2/pkg/runtime/events.go b/v2/pkg/runtime/events.go new file mode 100644 index 000000000..84aff7d74 --- /dev/null +++ b/v2/pkg/runtime/events.go @@ -0,0 +1,49 @@ +package runtime + +import ( + "context" +) + +// EventsOn registers a listener for the given event name. It returns a function to cancel the listener +func EventsOn(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() { + events := getEvents(ctx) + return events.On(eventName, callback) +} + +// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames` +func EventsOff(ctx context.Context, eventName string, additionalEventNames ...string) { + events := getEvents(ctx) + events.Off(eventName) + + if len(additionalEventNames) > 0 { + for _, eventName := range additionalEventNames { + events.Off(eventName) + } + } +} + +// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames` +func EventsOffAll(ctx context.Context) { + events := getEvents(ctx) + events.OffAll() +} + +// EventsOnce registers a listener for the given event name. After the first callback, the +// listener is deleted. It returns a function to cancel the listener +func EventsOnce(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() { + events := getEvents(ctx) + return events.Once(eventName, callback) +} + +// EventsOnMultiple registers a listener for the given event name, that may be called a maximum of 'counter' times. It returns a function +// to cancel the listener +func EventsOnMultiple(ctx context.Context, eventName string, callback func(optionalData ...interface{}), counter int) func() { + events := getEvents(ctx) + return events.OnMultiple(eventName, callback, counter) +} + +// EventsEmit pass through +func EventsEmit(ctx context.Context, eventName string, optionalData ...interface{}) { + events := getEvents(ctx) + events.Emit(eventName, optionalData...) +} diff --git a/v2/pkg/runtime/log.go b/v2/pkg/runtime/log.go new file mode 100644 index 000000000..3c2756f06 --- /dev/null +++ b/v2/pkg/runtime/log.go @@ -0,0 +1,105 @@ +package runtime + +import ( + "context" + "fmt" + + "github.com/wailsapp/wails/v2/pkg/logger" +) + +// LogPrint prints a Print level message +func LogPrint(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Print(message) +} + +// LogTrace prints a Trace level message +func LogTrace(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Trace(message) +} + +// LogDebug prints a Debug level message +func LogDebug(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Debug(message) +} + +// LogInfo prints a Info level message +func LogInfo(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Info(message) +} + +// LogWarning prints a Warning level message +func LogWarning(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Warning(message) +} + +// LogError prints a Error level message +func LogError(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Error(message) +} + +// LogFatal prints a Fatal level message +func LogFatal(ctx context.Context, message string) { + myLogger := getLogger(ctx) + myLogger.Fatal(message) +} + +// LogPrintf prints a Print level message +func LogPrintf(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Print(msg) +} + +// LogTracef prints a Trace level message +func LogTracef(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Trace(msg) +} + +// LogDebugf prints a Debug level message +func LogDebugf(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Debug(msg) +} + +// LogInfof prints a Info level message +func LogInfof(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Info(msg) +} + +// LogWarningf prints a Warning level message +func LogWarningf(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Warning(msg) +} + +// LogErrorf prints a Error level message +func LogErrorf(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Error(msg) +} + +// LogFatalf prints a Fatal level message +func LogFatalf(ctx context.Context, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + myLogger := getLogger(ctx) + myLogger.Fatal(msg) +} + +// LogSetLogLevel sets the log level +func LogSetLogLevel(ctx context.Context, level logger.LogLevel) { + myLogger := getLogger(ctx) + myLogger.SetLogLevel(level) +} diff --git a/v2/pkg/runtime/menu.go b/v2/pkg/runtime/menu.go new file mode 100644 index 000000000..09bd640c5 --- /dev/null +++ b/v2/pkg/runtime/menu.go @@ -0,0 +1,17 @@ +package runtime + +import ( + "context" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +func MenuSetApplicationMenu(ctx context.Context, menu *menu.Menu) { + frontend := getFrontend(ctx) + frontend.MenuSetApplicationMenu(menu) +} + +func MenuUpdateApplicationMenu(ctx context.Context) { + frontend := getFrontend(ctx) + frontend.MenuUpdateApplicationMenu() +} diff --git a/v2/pkg/runtime/runtime.go b/v2/pkg/runtime/runtime.go new file mode 100644 index 000000000..6de5ea798 --- /dev/null +++ b/v2/pkg/runtime/runtime.go @@ -0,0 +1,107 @@ +package runtime + +import ( + "context" + "log" + goruntime "runtime" + + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/logger" +) + +const contextError = `An invalid context was passed. This method requires the specific context given in the lifecycle hooks: +https://wails.io/docs/reference/runtime/intro` + +func getFrontend(ctx context.Context) frontend.Frontend { + if ctx == nil { + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + } + result := ctx.Value("frontend") + if result != nil { + return result.(frontend.Frontend) + } + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + return nil +} + +func getLogger(ctx context.Context) *logger.Logger { + if ctx == nil { + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + } + result := ctx.Value("logger") + if result != nil { + return result.(*logger.Logger) + } + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + return nil +} + +func getEvents(ctx context.Context) frontend.Events { + if ctx == nil { + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + } + result := ctx.Value("events") + if result != nil { + return result.(frontend.Events) + } + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("cannot call '%s': %s", funcName, contextError) + return nil +} + +// Quit the application +func Quit(ctx context.Context) { + if ctx == nil { + log.Fatalf("Error calling 'runtime.Quit': %s", contextError) + } + appFrontend := getFrontend(ctx) + appFrontend.Quit() +} + +// Hide the application +func Hide(ctx context.Context) { + if ctx == nil { + log.Fatalf("Error calling 'runtime.Hide': %s", contextError) + } + appFrontend := getFrontend(ctx) + appFrontend.Hide() +} + +// Show the application if it is hidden +func Show(ctx context.Context) { + if ctx == nil { + log.Fatalf("Error calling 'runtime.Show': %s", contextError) + } + appFrontend := getFrontend(ctx) + appFrontend.Show() +} + +// EnvironmentInfo contains information about the environment +type EnvironmentInfo struct { + BuildType string `json:"buildType"` + Platform string `json:"platform"` + Arch string `json:"arch"` +} + +// Environment returns information about the environment +func Environment(ctx context.Context) EnvironmentInfo { + var result EnvironmentInfo + buildType := ctx.Value("buildtype") + if buildType != nil { + result.BuildType = buildType.(string) + } + result.Platform = goruntime.GOOS + result.Arch = goruntime.GOARCH + return result +} diff --git a/v2/pkg/runtime/screen.go b/v2/pkg/runtime/screen.go new file mode 100644 index 000000000..c4d526692 --- /dev/null +++ b/v2/pkg/runtime/screen.go @@ -0,0 +1,15 @@ +package runtime + +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +type Screen = frontend.Screen + +// ScreenGetAll returns all screens +func ScreenGetAll(ctx context.Context) ([]Screen, error) { + appFrontend := getFrontend(ctx) + return appFrontend.ScreenGetAll() +} diff --git a/v2/pkg/runtime/signal_linux.go b/v2/pkg/runtime/signal_linux.go new file mode 100644 index 000000000..6a7ed5db3 --- /dev/null +++ b/v2/pkg/runtime/signal_linux.go @@ -0,0 +1,65 @@ +//go:build linux + +package runtime + +/* +#include +#include +#include +#include + +static void fix_signal(int signum) +{ + struct sigaction st; + + if (sigaction(signum, NULL, &st) < 0) { + return; + } + st.sa_flags |= SA_ONSTACK; + sigaction(signum, &st, NULL); +} + +static void fix_all_signals() +{ +#if defined(SIGSEGV) + fix_signal(SIGSEGV); +#endif +#if defined(SIGBUS) + fix_signal(SIGBUS); +#endif +#if defined(SIGFPE) + fix_signal(SIGFPE); +#endif +#if defined(SIGABRT) + fix_signal(SIGABRT); +#endif +} +*/ +import "C" + +// ResetSignalHandlers resets signal handlers to allow panic recovery. +// +// On Linux, WebKit (used for the webview) may install signal handlers without +// the SA_ONSTACK flag, which prevents Go from properly recovering from panics +// caused by nil pointer dereferences or other memory access violations. +// +// Call this function immediately before code that might panic to ensure +// the signal handlers are properly configured for Go's panic recovery mechanism. +// +// Example usage: +// +// go func() { +// defer func() { +// if err := recover(); err != nil { +// log.Printf("Recovered from panic: %v", err) +// } +// }() +// runtime.ResetSignalHandlers() +// // Code that might panic... +// }() +// +// Note: This function only has an effect on Linux. On other platforms, +// it is a no-op. +func ResetSignalHandlers() { + C.fix_all_signals() +} diff --git a/v2/pkg/runtime/signal_other.go b/v2/pkg/runtime/signal_other.go new file mode 100644 index 000000000..3171a700c --- /dev/null +++ b/v2/pkg/runtime/signal_other.go @@ -0,0 +1,18 @@ +//go:build !linux + +package runtime + +// ResetSignalHandlers resets signal handlers to allow panic recovery. +// +// On Linux, WebKit (used for the webview) may install signal handlers without +// the SA_ONSTACK flag, which prevents Go from properly recovering from panics +// caused by nil pointer dereferences or other memory access violations. +// +// Call this function immediately before code that might panic to ensure +// the signal handlers are properly configured for Go's panic recovery mechanism. +// +// Note: This function only has an effect on Linux. On other platforms, +// it is a no-op. +func ResetSignalHandlers() { + // No-op on non-Linux platforms +} diff --git a/v2/pkg/runtime/window.go b/v2/pkg/runtime/window.go new file mode 100644 index 000000000..62345e2e4 --- /dev/null +++ b/v2/pkg/runtime/window.go @@ -0,0 +1,186 @@ +package runtime + +import ( + "context" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +// WindowSetTitle sets the title of the window +func WindowSetTitle(ctx context.Context, title string) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetTitle(title) +} + +// WindowFullscreen makes the window fullscreen +func WindowFullscreen(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowFullscreen() +} + +// WindowUnfullscreen makes the window UnFullscreen +func WindowUnfullscreen(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowUnfullscreen() +} + +// WindowCenter the window on the current screen +func WindowCenter(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowCenter() +} + +// WindowReload will reload the window contents +func WindowReload(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowReload() +} + +// WindowReloadApp will reload the application +func WindowReloadApp(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowReloadApp() +} + +func WindowSetSystemDefaultTheme(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetSystemDefaultTheme() +} + +func WindowSetLightTheme(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetLightTheme() +} + +func WindowSetDarkTheme(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetDarkTheme() +} + +// WindowShow shows the window if hidden +func WindowShow(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowShow() +} + +// WindowHide the window +func WindowHide(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowHide() +} + +// WindowSetSize sets the size of the window +func WindowSetSize(ctx context.Context, width int, height int) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetSize(width, height) +} + +func WindowGetSize(ctx context.Context) (int, int) { + appFrontend := getFrontend(ctx) + return appFrontend.WindowGetSize() +} + +// WindowSetMinSize sets the minimum size of the window +func WindowSetMinSize(ctx context.Context, width int, height int) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetMinSize(width, height) +} + +// WindowSetMaxSize sets the maximum size of the window +func WindowSetMaxSize(ctx context.Context, width int, height int) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetMaxSize(width, height) +} + +// WindowSetAlwaysOnTop sets the window AlwaysOnTop or not on top +func WindowSetAlwaysOnTop(ctx context.Context, b bool) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetAlwaysOnTop(b) +} + +// WindowSetPosition sets the position of the window +func WindowSetPosition(ctx context.Context, x int, y int) { + appFrontend := getFrontend(ctx) + appFrontend.WindowSetPosition(x, y) +} + +func WindowGetPosition(ctx context.Context) (int, int) { + appFrontend := getFrontend(ctx) + return appFrontend.WindowGetPosition() +} + +// WindowMaximise the window +func WindowMaximise(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowMaximise() +} + +// WindowToggleMaximise the window +func WindowToggleMaximise(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowToggleMaximise() +} + +// WindowUnmaximise the window +func WindowUnmaximise(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowUnmaximise() +} + +// WindowMinimise the window +func WindowMinimise(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowMinimise() +} + +// WindowUnminimise the window +func WindowUnminimise(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowUnminimise() +} + +// WindowIsFullscreen get the window state is window Fullscreen +func WindowIsFullscreen(ctx context.Context) bool { + appFrontend := getFrontend(ctx) + return appFrontend.WindowIsFullscreen() +} + +// WindowIsMaximised get the window state is window Maximised +func WindowIsMaximised(ctx context.Context) bool { + appFrontend := getFrontend(ctx) + return appFrontend.WindowIsMaximised() +} + +// WindowIsMinimised get the window state is window Minimised +func WindowIsMinimised(ctx context.Context) bool { + appFrontend := getFrontend(ctx) + return appFrontend.WindowIsMinimised() +} + +// WindowIsNormal get the window state is window Normal +func WindowIsNormal(ctx context.Context) bool { + appFrontend := getFrontend(ctx) + return appFrontend.WindowIsNormal() +} + +// WindowExecJS executes the given Js in the window +func WindowExecJS(ctx context.Context, js string) { + appFrontend := getFrontend(ctx) + appFrontend.ExecJS(js) +} + +func WindowSetBackgroundColour(ctx context.Context, R, G, B, A uint8) { + appFrontend := getFrontend(ctx) + col := &options.RGBA{ + R: R, + G: G, + B: B, + A: A, + } + appFrontend.WindowSetBackgroundColour(col) +} + +func WindowPrint(ctx context.Context) { + appFrontend := getFrontend(ctx) + appFrontend.WindowPrint() +} diff --git a/v2/pkg/templates/base/.gitignore.tmpl b/v2/pkg/templates/base/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/base/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/base/README.md b/v2/pkg/templates/base/README.md new file mode 100644 index 000000000..abd8b9cd2 --- /dev/null +++ b/v2/pkg/templates/base/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails $NAME template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/base/app.tmpl.go b/v2/pkg/templates/base/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/base/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/base/go.mod.tmpl b/v2/pkg/templates/base/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/base/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/base/main.go.tmpl b/v2/pkg/templates/base/main.go.tmpl new file mode 100644 index 000000000..571cf6b10 --- /dev/null +++ b/v2/pkg/templates/base/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( +"embed" + +"github.com/wailsapp/wails/v2" +"github.com/wailsapp/wails/v2/pkg/options" +"github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed frontend/dist +var assets embed.FS + +func main() { +// Create an instance of the app structure +app := NewApp() + +// Create application with options +err := wails.Run(&options.App{ +Title: "{{.ProjectName}}", +Width: 1024, +Height: 768, +AssetServer: &assetserver.Options{ + Assets: assets, +}, +BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, +OnStartup: app.startup, +Bind: []interface{}{ +app, +}, +}) + +if err != nil { +println("Error:", err.Error()) +} +} diff --git a/v2/pkg/templates/base/template.json b/v2/pkg/templates/base/template.json new file mode 100644 index 000000000..8ba8f2193 --- /dev/null +++ b/v2/pkg/templates/base/template.json @@ -0,0 +1,7 @@ +{ + "name": "$NAME", + "shortname": "$SHORTNAME", + "author": "Lea Anthony", + "description": "$DESCRIPTION", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/base/wails.tmpl.json b/v2/pkg/templates/base/wails.tmpl.json new file mode 100644 index 000000000..ce4ffe365 --- /dev/null +++ b/v2/pkg/templates/base/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$scheme": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/generate/assets/common/.gitignore.tmpl b/v2/pkg/templates/generate/assets/common/.gitignore.tmpl new file mode 100644 index 000000000..d44c22f8c --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/common/frontend/dist/gitkeep b/v2/pkg/templates/generate/assets/common/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..01c74ee9b Binary files /dev/null and b/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/style.css b/v2/pkg/templates/generate/assets/common/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..febcb76cb --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + + + + + + diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts b/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts new file mode 100644 index 000000000..27fd71e45 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts @@ -0,0 +1,102 @@ +import {css, html, LitElement} from 'lit' +import logo from './assets/images/logo-universal.png' +import {Greet} from "../wailsjs/go/main/App"; +import {customElement, property} from 'lit/decorators.js' + +/** + * An example element. + * + * @slot - This element has a slot + * @csspart button - The button + */ +@customElement('my-element') +export class MyElement extends LitElement { + static styles = css` + #logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; + } + + .result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + } + + .input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; + } + + .input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; + } + + .input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; + } + + .input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + .input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + ` + + @property() + resultText = "Please enter your name below 👇" + + greet() { + let thisName = (this.shadowRoot?.getElementById('name') as HTMLInputElement)?.value; + if (thisName) { + Greet(thisName).then(result => { + this.resultText = result + }); + } + } + + render() { + return html` +
+ +
${this.resultText}
+
+ + +
+
+ ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'my-element': MyElement + } +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts b/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts new file mode 100644 index 000000000..bbb7f5889 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts @@ -0,0 +1,4 @@ +import {defineConfig} from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({}) diff --git a/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html new file mode 100644 index 000000000..fbe3eb240 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + + + + + + diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..01c74ee9b Binary files /dev/null and b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js b/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js new file mode 100644 index 000000000..ed65e2225 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js @@ -0,0 +1,105 @@ +import {css, html, LitElement} from 'lit' +import logo from './assets/images/logo-universal.png' +import {Greet} from "../wailsjs/go/main/App"; + +/** + * An example element. + * + * @slot - This element has a slot + * @csspart button - The button + */ +export class MyElement extends LitElement { + constructor() { + super() + this.resultText = "Please enter your name below 👇" + } + + static get styles() { + return css` + #logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; + } + + .result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + } + + .input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; + } + + .input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; + } + + .input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; + } + + .input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + .input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + ` + } + + static get properties() { + return { + resultText: {type: String}, + } + } + + greet() { + let thisName = this.shadowRoot.getElementById('name').value + Greet(thisName).then(result => { + this.resultText = result + }); + } + + render() { + return html` +
+ +
${this.resultText}
+
+ + +
+
+ ` + } + +} + +window.customElements.define('my-element', MyElement) diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/style.css b/v2/pkg/templates/generate/assets/lit/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/generate/assets/lit/frontend/vite.config.js b/v2/pkg/templates/generate/assets/lit/frontend/vite.config.js new file mode 100644 index 000000000..bbb7f5889 --- /dev/null +++ b/v2/pkg/templates/generate/assets/lit/frontend/vite.config.js @@ -0,0 +1,4 @@ +import {defineConfig} from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({}) diff --git a/v2/pkg/templates/generate/assets/preact-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/preact-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..0fb692c96 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.css b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.tsx b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.tsx new file mode 100644 index 000000000..bb50c5ec7 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/app.tsx @@ -0,0 +1,29 @@ +import './app.css' +import logo from "./assets/images/logo-universal.png" +import {Greet} from "../wailsjs/go/main/App"; +import {useState} from "preact/hooks"; + +export function App(props: any) { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e: any) => setName(e.target.value); + const updateResultText = (result: string) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( + <> +
+ +
{resultText}
+
+ + +
+
+ + ) +} diff --git a/v2/pkg/templates/generate/assets/preact-ts/frontend/src/main.tsx b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/main.tsx new file mode 100644 index 000000000..05b147282 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact-ts/frontend/src/main.tsx @@ -0,0 +1,5 @@ +import {render} from 'preact'; +import {App} from './app'; +import './style.css'; + +render(, document.getElementById('app')!); diff --git a/v2/pkg/templates/generate/assets/preact/frontend/dist/gitkeep b/v2/pkg/templates/generate/assets/preact/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/generate/assets/preact/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/preact/frontend/index.tmpl.html new file mode 100644 index 000000000..c8bfd4b76 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/app.css b/v2/pkg/templates/generate/assets/preact/frontend/src/app.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/src/app.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/app.jsx b/v2/pkg/templates/generate/assets/preact/frontend/src/app.jsx new file mode 100644 index 000000000..d0543d081 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/src/app.jsx @@ -0,0 +1,29 @@ +import './app.css'; +import logo from "./assets/images/logo-universal.png"; +import {Greet} from "../wailsjs/go/main/App"; +import {useState} from "preact/hooks"; + +export function App(props) { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e) => setName(e.target.value); + const updateResultText = (result) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( + <> +
+ +
{resultText}
+
+ + +
+
+ + ) +} diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..01c74ee9b Binary files /dev/null and b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx b/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx new file mode 100644 index 000000000..6c42a5949 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx @@ -0,0 +1,5 @@ +import {render} from 'preact'; +import {App} from './app'; +import './style.css'; + +render(, document.getElementById('app')); \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/style.css b/v2/pkg/templates/generate/assets/preact/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/generate/assets/preact/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..a2023cac7 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx new file mode 100644 index 000000000..a6e56f9f8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx @@ -0,0 +1,28 @@ +import {useState} from 'react'; +import logo from './assets/images/logo-universal.png'; +import './App.css'; +import {Greet} from "../wailsjs/go/main/App"; + +function App() { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e: any) => setName(e.target.value); + const updateResultText = (result: string) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( +
+ +
{resultText}
+
+ + +
+
+ ) +} + +export default App diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/src/main.tsx b/v2/pkg/templates/generate/assets/react-ts/frontend/src/main.tsx new file mode 100644 index 000000000..3626ff303 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react-ts/frontend/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import {createRoot} from 'react-dom/client' +import './style.css' +import App from './App' + +const container = document.getElementById('root') + +const root = createRoot(container!) + +root.render( + + + +) diff --git a/v2/pkg/templates/generate/assets/react/frontend/dist/gitkeep b/v2/pkg/templates/generate/assets/react/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/generate/assets/react/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/react/frontend/index.tmpl.html new file mode 100644 index 000000000..80aa30b89 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/App.css b/v2/pkg/templates/generate/assets/react/frontend/src/App.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/src/App.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/App.jsx b/v2/pkg/templates/generate/assets/react/frontend/src/App.jsx new file mode 100644 index 000000000..fd762291f --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/src/App.jsx @@ -0,0 +1,28 @@ +import {useState} from 'react'; +import logo from './assets/images/logo-universal.png'; +import './App.css'; +import {Greet} from "../wailsjs/go/main/App"; + +function App() { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e) => setName(e.target.value); + const updateResultText = (result) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( +
+ +
{resultText}
+
+ + +
+
+ ) +} + +export default App diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..01c74ee9b Binary files /dev/null and b/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx b/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx new file mode 100644 index 000000000..e50e105db --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx @@ -0,0 +1,14 @@ +import React from 'react' +import {createRoot} from 'react-dom/client' +import './style.css' +import App from './App' + +const container = document.getElementById('root') + +const root = createRoot(container) + +root.render( + + + +) diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/style.css b/v2/pkg/templates/generate/assets/react/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/generate/assets/react/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..3dd212f2d --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte new file mode 100644 index 000000000..1987eb090 --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte @@ -0,0 +1,79 @@ + + +
+ +
{resultText}
+
+ + +
+
+ + diff --git a/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/main.ts b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/main.ts new file mode 100644 index 000000000..95c41a51d --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/main.ts @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/v2/pkg/templates/generate/assets/svelte/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/svelte/frontend/index.tmpl.html new file mode 100644 index 000000000..859919153 --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/generate/assets/svelte/frontend/src/App.svelte b/v2/pkg/templates/generate/assets/svelte/frontend/src/App.svelte new file mode 100644 index 000000000..2a2ce2282 --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte/frontend/src/App.svelte @@ -0,0 +1,79 @@ + + +
+ +
{resultText}
+
+ + +
+
+ + diff --git a/v2/pkg/templates/generate/assets/svelte/frontend/src/main.js b/v2/pkg/templates/generate/assets/svelte/frontend/src/main.js new file mode 100644 index 000000000..95c41a51d --- /dev/null +++ b/v2/pkg/templates/generate/assets/svelte/frontend/src/main.js @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/v2/pkg/templates/generate/assets/vanilla-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..3dd212f2d --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/app.css b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/main.ts b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/main.ts new file mode 100644 index 000000000..b68d7d961 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla-ts/frontend/src/main.ts @@ -0,0 +1,49 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement!.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement!.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +document.querySelector('#app')!.innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +(document.getElementById('logo') as HTMLImageElement).src = logo; + +let nameElement = (document.getElementById("name") as HTMLInputElement); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +declare global { + interface Window { + greet: () => void; + } +} diff --git a/v2/pkg/templates/generate/assets/vanilla/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vanilla/frontend/index.tmpl.html new file mode 100644 index 000000000..859919153 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/generate/assets/vanilla/frontend/src/app.css b/v2/pkg/templates/generate/assets/vanilla/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/generate/assets/vanilla/frontend/src/main.js b/v2/pkg/templates/generate/assets/vanilla/frontend/src/main.js new file mode 100644 index 000000000..4ad5a2cae --- /dev/null +++ b/v2/pkg/templates/generate/assets/vanilla/frontend/src/main.js @@ -0,0 +1,43 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..cc259435b --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue new file mode 100644 index 000000000..b63d187c5 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue new file mode 100644 index 000000000..3ab3df798 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts new file mode 100644 index 000000000..e57db5948 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts @@ -0,0 +1,4 @@ +import {createApp} from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json b/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json new file mode 100644 index 000000000..3cc844d92 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": [ + "ESNext", + "DOM" + ], + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html new file mode 100644 index 000000000..d45b7a8c4 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue b/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue new file mode 100644 index 000000000..15d2f1215 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..b1224ec79 Binary files /dev/null and b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue new file mode 100644 index 000000000..29c023fbe --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/main.js b/v2/pkg/templates/generate/assets/vue/frontend/src/main.js new file mode 100644 index 000000000..e57db5948 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/src/main.js @@ -0,0 +1,4 @@ +import {createApp} from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/style.css b/v2/pkg/templates/generate/assets/vue/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js b/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js new file mode 100644 index 000000000..a30c338ed --- /dev/null +++ b/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) diff --git a/v2/pkg/templates/generate/generate.go b/v2/pkg/templates/generate/generate.go new file mode 100644 index 000000000..6842dc196 --- /dev/null +++ b/v2/pkg/templates/generate/generate.go @@ -0,0 +1,232 @@ +package main + +import ( + "embed" + "os" + "strings" + + "github.com/leaanthony/debme" + "github.com/leaanthony/gosod" + "github.com/wailsapp/wails/v2/internal/s" +) + +//go:embed assets/common/* +var common embed.FS + +//go:embed assets/svelte/* +var svelte embed.FS + +//go:embed assets/svelte-ts/* +var sveltets embed.FS + +//go:embed assets/lit/* +var lit embed.FS + +//go:embed assets/lit-ts/* +var litts embed.FS + +//go:embed assets/vue/* +var vue embed.FS + +//go:embed assets/vue-ts/* +var vuets embed.FS + +//go:embed assets/react/* +var react embed.FS + +//go:embed assets/react-ts/* +var reactts embed.FS + +//go:embed assets/preact/* +var preact embed.FS + +//go:embed assets/preact-ts/* +var preactts embed.FS + +//go:embed assets/vanilla/* +var vanilla embed.FS + +//go:embed assets/vanilla-ts/* +var vanillats embed.FS + +func checkError(err error) { + if err != nil { + println("\nERROR:", err.Error()) + os.Exit(1) + } +} + +type template struct { + Name string + ShortName string + Description string + Assets embed.FS + FilesToDelete []string + DirsToDelete []string +} + +var templates = []*template{ + { + Name: "Svelte + Vite", + ShortName: "Svelte", + Description: "Svelte + Vite development server", + Assets: svelte, + FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore", "frontend/src/app.css", "frontend/src/assets/svelte.svg"}, + DirsToDelete: []string{"frontend/public", "frontend/src/lib"}, + }, + { + Name: "Svelte + Vite (Typescript)", + ShortName: "Svelte-TS", + Description: "Svelte + TS + Vite development server", + Assets: sveltets, + FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore", "frontend/src/app.css", "frontend/src/assets/svelte.svg"}, + DirsToDelete: []string{"frontend/public", "frontend/src/lib"}, + }, + { + Name: "Lit + Vite", + ShortName: "Lit", + Description: "Lit + Vite development server", + Assets: lit, + FilesToDelete: []string{"frontend/index.html", "frontend/vite.config.js"}, + }, + { + Name: "Lit + Vite (Typescript)", + ShortName: "Lit-TS", + Description: "Lit + TS + Vite development server", + Assets: litts, + FilesToDelete: []string{"frontend/index.html", "frontend/src/favicon.svg"}, + }, + { + Name: "Vue + Vite", + ShortName: "Vue", + Description: "Vue + Vite development server", + Assets: vue, + FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore"}, + DirsToDelete: []string{"frontend/src/assets", "frontend/src/components", "frontend/public"}, + }, + { + Name: "Vue + Vite (Typescript)", + ShortName: "Vue-TS", + Description: "Vue + Vite development server", + Assets: vuets, + FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore"}, + DirsToDelete: []string{"frontend/src/assets", "frontend/src/components", "frontend/public"}, + }, + { + Name: "React + Vite", + ShortName: "React", + Description: "React + Vite development server", + Assets: react, + FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.svg", "frontend/.gitignore", "frontend/index.html"}, + }, + { + Name: "React + Vite (Typescript)", + ShortName: "React-TS", + Description: "React + Vite development server", + Assets: reactts, + FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.svg", "frontend/.gitignore", "frontend/index.html"}, + }, + { + Name: "Preact + Vite", + ShortName: "Preact", + Description: "Preact + Vite development server", + Assets: preact, + FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.jsx", "frontend/.gitignore", "frontend/index.html"}, + DirsToDelete: []string{"frontend/public"}, + }, + { + Name: "Preact + Vite (Typescript)", + ShortName: "Preact-TS", + Description: "Preact + Vite development server", + Assets: preactts, + FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/assets/preact.svg", "frontend/src/logo.tsx", "frontend/.gitignore", "frontend/index.html"}, + DirsToDelete: []string{"frontend/public"}, + }, + { + Name: "Vanilla + Vite", + ShortName: "Vanilla", + Description: "Vanilla + Vite development server", + Assets: vanilla, + FilesToDelete: []string{"frontend/.gitignore", "frontend/index.html", "frontend/favicon.svg", "frontend/main.js", "frontend/style.css"}, + }, + { + Name: "Vanilla + Vite (Typescript)", + ShortName: "Vanilla-TS", + Description: "Vanilla + Vite development server", + Assets: vanillats, + FilesToDelete: []string{"frontend/.gitignore", "frontend/index.html", "frontend/favicon.svg", "frontend/src/main.ts", "frontend/src/style.css"}, + }, +} + +func main() { + rebuildRuntime() + + for _, t := range templates { + createTemplate(t) + } + + // copy plain template + s.ECHO("Copying plain template") + s.RMDIR("../templates/plain") + s.COPYDIR("plain", "../templates/plain") + + s.ECHO(`Until an auto fix is done, add "@babel/types": "^7.17.10" to vue-ts/frontend/package.json`) +} + +func rebuildRuntime() { + s.ECHO("Generating Runtime") + cwd := s.CWD() + const runtimeDir = "../../../internal/frontend/runtime/" + const commonDir = "./assets/common/frontend/wailsjs/runtime/" + s.CD(runtimeDir) + s.EXEC("npm run build") + s.ECHO("Copying new files") + s.CD("wrapper") + s.COPY("package.json", commonDir+"package.json") + s.COPY("runtime.js", commonDir+"runtime.js") + s.COPY("runtime.d.ts", commonDir+"runtime.d.ts") + s.CD(cwd) +} + +func createTemplate(template *template) { + cwd := s.CWD() + name := template.Name + shortName := strings.ToLower(template.ShortName) + assets, err := debme.FS(template.Assets, "assets/"+shortName) + checkError(err) + commonAssets, err := debme.FS(common, "assets/common") + checkError(err) + + s.CD("..") + s.ENDIR("templates") + s.CD("templates") + s.RMDIR(shortName) + s.COPYDIR("../base", shortName) + s.CD(shortName) + s.ECHO("Generating vite template: " + shortName) + s.EXEC("npm create vite@latest frontend --template " + shortName) + + // Clean up template + for _, fileToDelete := range template.FilesToDelete { + s.DELETE(fileToDelete) + } + for _, dirToDelete := range template.DirsToDelete { + s.RMDIR(dirToDelete) + } + s.REPLACEALL("README.md", s.Sub{"$NAME": template.ShortName}) + s.REPLACEALL("template.json", s.Sub{"$NAME": name, "$SHORTNAME": shortName, "$DESCRIPTION": template.Description}) + + // Add common files + g := gosod.New(commonAssets) + g.SetTemplateFilters([]string{}) + err = g.Extract(".", nil) + checkError(err) + + // Add custom files + g = gosod.New(assets) + g.SetTemplateFilters([]string{}) + err = g.Extract(".", nil) + checkError(err) + + s.CD(cwd) +} diff --git a/v2/pkg/templates/generate/go.sum b/v2/pkg/templates/generate/go.sum new file mode 100644 index 000000000..69c3ba18a --- /dev/null +++ b/v2/pkg/templates/generate/go.sum @@ -0,0 +1,4 @@ +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= diff --git a/v2/pkg/templates/generate/plain/.gitignore.tmpl b/v2/pkg/templates/generate/plain/.gitignore.tmpl new file mode 100644 index 000000000..b92a6f8bf --- /dev/null +++ b/v2/pkg/templates/generate/plain/.gitignore.tmpl @@ -0,0 +1,12 @@ +# Wails bin directory +build/bin +# Wails Windows NSIS support files +build/windows/installer/wails_tools.nsh +build/windows/installer/tmp/ + +# IDEs +.idea +.vscode + +# The black hole that is... +node_modules diff --git a/v2/pkg/templates/generate/plain/README.md b/v2/pkg/templates/generate/plain/README.md new file mode 100644 index 000000000..9fcd85bdd --- /dev/null +++ b/v2/pkg/templates/generate/plain/README.md @@ -0,0 +1,18 @@ +# README + +## About + +This template uses plain JS / HTML and CSS. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. The frontend dev server will run +on http://localhost:34115. Open this in your browser to connect to your application. + +## Building + +For a production build, use `wails build`. + diff --git a/v2/pkg/templates/generate/plain/app.go b/v2/pkg/templates/generate/plain/app.go new file mode 100644 index 000000000..224be7156 --- /dev/null +++ b/v2/pkg/templates/generate/plain/app.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called at application startup +func (a *App) startup(ctx context.Context) { + // Perform your setup here + a.ctx = ctx +} + +// domReady is called after front-end resources have been loaded +func (a App) domReady(ctx context.Context) { + // Add your action here +} + +// beforeClose is called when the application is about to quit, +// either by clicking the window close button or calling runtime.Quit. +// Returning true will cause the application to continue, false will continue shutdown as normal. +func (a *App) beforeClose(ctx context.Context) (prevent bool) { + return false +} + +// shutdown is called at application termination +func (a *App) shutdown(ctx context.Context) { + // Perform your teardown here +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..b1224ec79 Binary files /dev/null and b/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html b/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html new file mode 100644 index 000000000..a8a434a37 --- /dev/null +++ b/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html @@ -0,0 +1,21 @@ + + + + + + {{.ProjectName}} + + + + +
+ +
Please enter your name below 👇
+
+ + +
+
+ + + diff --git a/v2/pkg/templates/generate/plain/frontend/src/main.css b/v2/pkg/templates/generate/plain/frontend/src/main.css new file mode 100644 index 000000000..dab87d09a --- /dev/null +++ b/v2/pkg/templates/generate/plain/frontend/src/main.css @@ -0,0 +1,82 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} + +.logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-image: url("./assets/images/logo-universal.png"); + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} diff --git a/v2/pkg/templates/generate/plain/frontend/src/main.js b/v2/pkg/templates/generate/plain/frontend/src/main.js new file mode 100644 index 000000000..3346d59ff --- /dev/null +++ b/v2/pkg/templates/generate/plain/frontend/src/main.js @@ -0,0 +1,32 @@ +// Get input + focus +let nameElement = document.getElementById("name"); +nameElement.focus(); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + window.go.main.App.Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +nameElement.onkeydown = function (e) { + if (e.keyCode == 13) { + window.greet(); + } +}; diff --git a/v2/pkg/templates/generate/plain/go.mod.tmpl b/v2/pkg/templates/generate/plain/go.mod.tmpl new file mode 100644 index 000000000..f6d0daec4 --- /dev/null +++ b/v2/pkg/templates/generate/plain/go.mod.tmpl @@ -0,0 +1,34 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +require ( +github.com/andybalholm/brotli v1.0.2 // indirect +github.com/davecgh/go-spew v1.1.1 // indirect +github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab // indirect +github.com/wailsapp/mimetype v1.4.1-beta.1 +github.com/go-ole/go-ole v1.2.5 // indirect +github.com/gofiber/fiber/v2 v2.17.0 // indirect +github.com/gofiber/websocket/v2 v2.0.8 // indirect +github.com/google/uuid v1.1.2 // indirect +github.com/imdario/mergo v0.3.12 // indirect +github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 // indirect +github.com/klauspost/compress v1.12.2 // indirect +github.com/leaanthony/debme v1.2.1 // indirect +github.com/leaanthony/go-ansi-parser v1.0.1 // indirect +github.com/leaanthony/slicer v1.5.0 // indirect +github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect +github.com/pkg/errors v0.9.1 // indirect +github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f // indirect +github.com/tkrajina/go-reflector v0.5.5 // indirect +github.com/valyala/bytebufferpool v1.0.0 // indirect +github.com/valyala/fasthttp v1.28.0 // indirect +github.com/valyala/tcplisten v1.0.0 // indirect +golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect +) + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} diff --git a/v2/pkg/templates/generate/plain/go.sum b/v2/pkg/templates/generate/plain/go.sum new file mode 100644 index 000000000..3e14e745f --- /dev/null +++ b/v2/pkg/templates/generate/plain/go.sum @@ -0,0 +1,220 @@ +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab h1:9e2joQGp642wHGFP5m86SDptAavrdGBe8/x9DGEEAaI= +github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/wailsapp/mimetype v1.4.1-beta.1 h1:gSnKX7WH+7aA0EEjOGUmpWXTb0Nt5B7/8Dm9wHLrnnY= +github.com/wailsapp/mimetype v1.4.1-beta.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gofiber/fiber/v2 v2.17.0 h1:qP3PkGUbBB0i9iQh5E057XI1yO5CZigUxZhyUFYAFoM= +github.com/gofiber/fiber/v2 v2.17.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg= +github.com/gofiber/websocket/v2 v2.0.8 h1:Hb4y6IxYZVMO0segROODXJiXVgVD3a6i7wnfot8kM6k= +github.com/gofiber/websocket/v2 v2.0.8/go.mod h1:fv8HSGQX09sauNv9g5Xq8GeGAaahLFYQKKb4ZdT0x2w= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= +github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= +github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= +github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= +github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+aPcuJ5Y/hpjq/lmgMFmpjwl0= +github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js= +github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f h1:PgA+Olipyj258EIEYnpFFONrrCcAIWNUNoFhUfMqAGY= +github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A= +github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= +github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= +github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs= +github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= +github.com/valyala/fasthttp v1.28.0 h1:ruVmTmZaBR5i67NqnjvvH5gEv0zwHfWtbjoyW98iho4= +github.com/valyala/fasthttp v1.28.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/wailsapp/wails/v2 v2.0.0-beta.3 h1:8vhBbnjpYDF6cCUwKadon7J/98UjcP1nrnptUl70Tfg= +github.com/wailsapp/wails/v2 v2.0.0-beta.3/go.mod h1:aku28riyHF2G5jmx/qtxjLWi7VwpTjhhX/HVLCptWFA= +github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= +github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/v2/pkg/templates/generate/plain/main.go.tmpl b/v2/pkg/templates/generate/plain/main.go.tmpl new file mode 100644 index 000000000..9f3e2fffe --- /dev/null +++ b/v2/pkg/templates/generate/plain/main.go.tmpl @@ -0,0 +1,86 @@ +package main + +import ( +"embed" +"log" + +"github.com/wailsapp/wails/v2/pkg/options/mac" + +"github.com/wailsapp/wails/v2" +"github.com/wailsapp/wails/v2/pkg/logger" +"github.com/wailsapp/wails/v2/pkg/options" +"github.com/wailsapp/wails/v2/pkg/options/assetserver" +"github.com/wailsapp/wails/v2/pkg/options/windows" +) + +//go:embed frontend/src +var assets embed.FS + +//go:embed build/appicon.png +var icon []byte + +func main() { +// Create an instance of the app structure +app := NewApp() + +// Create application with options +err := wails.Run(&options.App{ +Title: "{{.ProjectName}}", +Width: 1024, +Height: 768, +MinWidth: 1024, +MinHeight: 768, +MaxWidth: 1280, +MaxHeight: 800, +DisableResize: false, +Fullscreen: false, +Frameless: false, +StartHidden: false, +HideWindowOnClose: false, +BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, +AssetServer: &assetserver.Options{ + Assets: assets, +}, +Menu: nil, +Logger: nil, +LogLevel: logger.DEBUG, +OnStartup: app.startup, +OnDomReady: app.domReady, +OnBeforeClose: app.beforeClose, +OnShutdown: app.shutdown, +WindowStartState: options.Normal, +Bind: []interface{}{ +app, +}, +// Windows platform specific options +Windows: &windows.Options{ +WebviewIsTransparent: false, +WindowIsTranslucent: false, +DisableWindowIcon: false, +// DisableFramelessWindowDecorations: false, +WebviewUserDataPath: "", +}, +Mac: &mac.Options{ +TitleBar: &mac.TitleBar{ +TitlebarAppearsTransparent: true, +HideTitle: false, +HideTitleBar: false, +FullSizeContent: false, +UseToolbar: false, +HideToolbarSeparator: true, +}, +Appearance: mac.NSAppearanceNameDarkAqua, +WebviewIsTransparent: true, +WindowIsTranslucent: true, +About: &mac.AboutInfo{ +Title: "Plain Template", +Message: "Part of the Wails projects", +Icon: icon, +}, +}, +}) + +if err != nil { +log.Fatal(err) +} +} diff --git a/v2/pkg/templates/generate/plain/template.json b/v2/pkg/templates/generate/plain/template.json new file mode 100644 index 000000000..fc919bc3b --- /dev/null +++ b/v2/pkg/templates/generate/plain/template.json @@ -0,0 +1,7 @@ +{ + "name": "Plain HTML/JS/CSS", + "shortname": "plain", + "author": "Lea Anthony ", + "description": "A simple template using only HTML/CSS/JS", + "helpurl": "https://github.com/wailsapp/wails" +} diff --git a/v2/pkg/templates/generate/plain/wails.tmpl.json b/v2/pkg/templates/generate/plain/wails.tmpl.json new file mode 100644 index 000000000..0168826bd --- /dev/null +++ b/v2/pkg/templates/generate/plain/wails.tmpl.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "wailsjsdir": "./frontend", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/ides/goland/gitignore.txt b/v2/pkg/templates/ides/goland/gitignore.txt new file mode 100644 index 000000000..73f69e095 --- /dev/null +++ b/v2/pkg/templates/ides/goland/gitignore.txt @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/v2/pkg/templates/ides/goland/modules.tmpl.xml b/v2/pkg/templates/ides/goland/modules.tmpl.xml new file mode 100644 index 000000000..228a50071 --- /dev/null +++ b/v2/pkg/templates/ides/goland/modules.tmpl.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/v2/pkg/templates/ides/goland/name.tmpl b/v2/pkg/templates/ides/goland/name.tmpl new file mode 100644 index 000000000..8c328a5d3 --- /dev/null +++ b/v2/pkg/templates/ides/goland/name.tmpl @@ -0,0 +1 @@ +{{.ProjectName}} \ No newline at end of file diff --git a/v2/pkg/templates/ides/goland/projectname.iml b/v2/pkg/templates/ides/goland/projectname.iml new file mode 100644 index 000000000..5e764c4f0 --- /dev/null +++ b/v2/pkg/templates/ides/goland/projectname.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/v2/pkg/templates/ides/goland/vcs.xml b/v2/pkg/templates/ides/goland/vcs.xml new file mode 100644 index 000000000..78f5bc6f7 --- /dev/null +++ b/v2/pkg/templates/ides/goland/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/v2/pkg/templates/ides/goland/workspace.tmpl.xml b/v2/pkg/templates/ides/goland/workspace.tmpl.xml new file mode 100644 index 000000000..27f8881bf --- /dev/null +++ b/v2/pkg/templates/ides/goland/workspace.tmpl.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + \ No newline at end of file diff --git a/v2/pkg/templates/ides/vscode/launch.tmpl.json b/v2/pkg/templates/ides/vscode/launch.tmpl.json new file mode 100644 index 000000000..0a5437c9e --- /dev/null +++ b/v2/pkg/templates/ides/vscode/launch.tmpl.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Wails: Production {{.ProjectName}}", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/{{.PathToDesktopBinary}}", + "preLaunchTask": "build", + "cwd": "${workspaceFolder}" + }, + { + "name": "Wails: Debug {{.ProjectName}}", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/{{.PathToDesktopBinary}}", + "preLaunchTask": "build debug", + "cwd": "${workspaceFolder}" + }, + { + "name": "Wails: Dev {{.ProjectName}}", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/{{.PathToDesktopBinary}}", + "preLaunchTask": "build dev", + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/v2/pkg/templates/ides/vscode/tasks.tmpl.json b/v2/pkg/templates/ides/vscode/tasks.tmpl.json new file mode 100644 index 000000000..fdf1d48dd --- /dev/null +++ b/v2/pkg/templates/ides/vscode/tasks.tmpl.json @@ -0,0 +1,111 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CGO_ENABLED": "1" + } + }, + "osx": { + "options": { + "env": { + "CGO_CFLAGS": "-mmacosx-version-min=10.13", + "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13" + } + } + }, + "windows": { + "options": { + "env": { + "CGO_ENABLED": "0" + } + } + }, + "command": "go", + "args": [ + "build", + "-tags", + "production,desktop", + "-gcflags", + "all=-N -l", + "-o", + "{{.PathToDesktopBinary}}" + ] + }, + { + "label": "build debug", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CGO_ENABLED": "1" + } + }, + "osx": { + "options": { + "env": { + "CGO_CFLAGS": "-mmacosx-version-min=10.13", + "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13" + } + } + }, + "windows": { + "options": { + "env": { + "CGO_ENABLED": "0" + } + } + }, + "command": "go", + "args": [ + "build", + "-tags", + "production,desktop,debug", + "-gcflags", + "all=-N -l", + "-o", + "{{.PathToDesktopBinary}}" + ] + }, + { + "label": "build dev", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CGO_ENABLED": "1" + } + }, + "osx": { + "options": { + "env": { + "CGO_CFLAGS": "-mmacosx-version-min=10.13", + "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13" + } + } + }, + "windows": { + "options": { + "env": { + "CGO_ENABLED": "0" + } + } + }, + "command": "go", + "args": [ + "build", + "-tags", + "dev", + "-gcflags", + "all=-N -l", + "-o", + "{{.PathToDesktopBinary}}" + ] + } + ] +} + \ No newline at end of file diff --git a/v2/pkg/templates/templates.go b/v2/pkg/templates/templates.go new file mode 100644 index 000000000..e18185520 --- /dev/null +++ b/v2/pkg/templates/templates.go @@ -0,0 +1,407 @@ +package templates + +import ( + "embed" + "encoding/json" + "fmt" + gofs "io/fs" + "log" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/pkg/errors" + + "github.com/leaanthony/debme" + "github.com/leaanthony/gosod" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/pkg/clilogger" +) + +//go:embed all:templates +var templates embed.FS + +//go:embed all:ides/* +var ides embed.FS + +// Cahce for the templates +// We use this because we need different views of the same data +var templateCache []Template = nil + +// Data contains the data we wish to embed during template installation +type Data struct { + ProjectName string + BinaryName string + WailsVersion string + NPMProjectName string + AuthorName string + AuthorEmail string + AuthorNameAndEmail string + WailsDirectory string + GoSDKPath string + WindowsFlags string + CGOEnabled string + OutputFile string +} + +// Options for installing a template +type Options struct { + ProjectName string + TemplateName string + BinaryName string + TargetDir string + Logger *clilogger.CLILogger + PathToDesktopBinary string + PathToServerBinary string + InitGit bool + AuthorName string + AuthorEmail string + IDE string + ProjectNameFilename string // The project name but as a valid filename + WailsVersion string + GoSDKPath string + WindowsFlags string + CGOEnabled string + CGOLDFlags string + OutputFile string +} + +// Template holds data relating to a template +// including the metadata stored in template.json +type Template struct { + // Template details + Name string `json:"name"` + ShortName string `json:"shortname"` + Author string `json:"author"` + Description string `json:"description"` + HelpURL string `json:"helpurl"` + + // Other data + FS gofs.FS `json:"-"` +} + +func parseTemplate(template gofs.FS) (Template, error) { + var result Template + data, err := gofs.ReadFile(template, "template.json") + if err != nil { + return result, errors.Wrap(err, "Error parsing template") + } + err = json.Unmarshal(data, &result) + if err != nil { + return result, err + } + result.FS = template + return result, nil +} + +// List returns the list of available templates +func List() ([]Template, error) { + // If the cache isn't loaded, load it + if templateCache == nil { + err := loadTemplateCache() + if err != nil { + return nil, err + } + } + + return templateCache, nil +} + +// getTemplateByShortname returns the template with the given short name +func getTemplateByShortname(shortname string) (Template, error) { + var result Template + + // If the cache isn't loaded, load it + if templateCache == nil { + err := loadTemplateCache() + if err != nil { + return result, err + } + } + + for _, template := range templateCache { + if template.ShortName == shortname { + return template, nil + } + } + + return result, fmt.Errorf("shortname '%s' is not a valid template shortname", shortname) +} + +// Loads the template cache +func loadTemplateCache() error { + templatesFS, err := debme.FS(templates, "templates") + if err != nil { + return err + } + + // Get directories + files, err := templatesFS.ReadDir(".") + if err != nil { + return err + } + + // Reset cache + templateCache = []Template{} + + for _, file := range files { + if file.IsDir() { + templateFS, err := templatesFS.FS(file.Name()) + if err != nil { + return err + } + template, err := parseTemplate(templateFS) + if err != nil { + // Cannot parse this template, continue + continue + } + templateCache = append(templateCache, template) + } + } + + return nil +} + +// Install the given template. Returns true if the template is remote. +func Install(options *Options) (bool, *Template, error) { + // Get cwd + cwd, err := os.Getwd() + if err != nil { + return false, nil, err + } + + // Did the user want to install in current directory? + if options.TargetDir == "" { + options.TargetDir = filepath.Join(cwd, options.ProjectName) + if fs.DirExists(options.TargetDir) { + return false, nil, fmt.Errorf("cannot create project directory. Dir exists: %s", options.TargetDir) + } + } else { + // Get the absolute path of the given directory + targetDir, err := filepath.Abs(options.TargetDir) + if err != nil { + return false, nil, err + } + options.TargetDir = targetDir + if fs.DirExists(options.TargetDir) { + // Check if directory is non-empty + entries, err := os.ReadDir(options.TargetDir) + if err != nil { + return false, nil, err + } + if len(entries) > 0 { + return false, nil, fmt.Errorf("cannot initialise project in non-empty directory: %s", options.TargetDir) + } + } else { + err := fs.Mkdir(options.TargetDir) + if err != nil { + return false, nil, err + } + } + } + + // Flag to indicate remote template + remoteTemplate := false + + // Is this a shortname? + template, err := getTemplateByShortname(options.TemplateName) + if err != nil { + // Is this a filepath? + templatePath, err := filepath.Abs(options.TemplateName) + if fs.DirExists(templatePath) { + templateFS := os.DirFS(templatePath) + template, err = parseTemplate(templateFS) + if err != nil { + return false, nil, errors.Wrap(err, "Error installing template") + } + } else { + // git clone to temporary dir + tempdir, err := gitclone(options) + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Fatal(err) + } + }(tempdir) + if err != nil { + return false, nil, err + } + // Remove the .git directory + err = os.RemoveAll(filepath.Join(tempdir, ".git")) + if err != nil { + return false, nil, err + } + + templateFS := os.DirFS(tempdir) + template, err = parseTemplate(templateFS) + if err != nil { + return false, nil, err + } + remoteTemplate = true + } + } + + // Use Gosod to install the template + installer := gosod.New(template.FS) + + // Ignore template.json files + installer.IgnoreFile("template.json") + + // Setup the data. + // We use the directory name for the binary name, like Go + BinaryName := filepath.Base(options.TargetDir) + NPMProjectName := strings.ToLower(strings.ReplaceAll(BinaryName, " ", "")) + localWailsDirectory := fs.RelativePath("../../../../../..") + + templateData := &Data{ + ProjectName: options.ProjectName, + BinaryName: filepath.Base(options.TargetDir), + NPMProjectName: NPMProjectName, + WailsDirectory: localWailsDirectory, + AuthorEmail: options.AuthorEmail, + AuthorName: options.AuthorName, + WailsVersion: options.WailsVersion, + GoSDKPath: options.GoSDKPath, + } + + // Create a formatted name and email combo. + if options.AuthorName != "" { + templateData.AuthorNameAndEmail = options.AuthorName + " " + } + if options.AuthorEmail != "" { + templateData.AuthorNameAndEmail += "<" + options.AuthorEmail + ">" + } + templateData.AuthorNameAndEmail = strings.TrimSpace(templateData.AuthorNameAndEmail) + + installer.RenameFiles(map[string]string{ + "gitignore.txt": ".gitignore", + }) + + // Extract the template + err = installer.Extract(options.TargetDir, templateData) + if err != nil { + return false, nil, err + } + + err = generateIDEFiles(options) + if err != nil { + return false, nil, err + } + + return remoteTemplate, &template, nil +} + +// Clones the given uri and returns the temporary cloned directory +func gitclone(options *Options) (string, error) { + // Create temporary directory + dirname, err := os.MkdirTemp("", "wails-template-*") + if err != nil { + return "", err + } + + // Parse remote template url and version number + templateInfo := strings.Split(options.TemplateName, "@") + cloneOption := &git.CloneOptions{ + URL: templateInfo[0], + } + if len(templateInfo) > 1 { + cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1]) + } + + _, err = git.PlainClone(dirname, false, cloneOption) + + return dirname, err +} + +func generateIDEFiles(options *Options) error { + switch options.IDE { + case "vscode": + return generateVSCodeFiles(options) + case "goland": + return generateGolandFiles(options) + } + + return nil +} + +type ideOptions struct { + name string + targetDir string + options *Options + renameFiles map[string]string + ignoredFiles []string +} + +func generateGolandFiles(options *Options) error { + ideoptions := ideOptions{ + name: "goland", + targetDir: filepath.Join(options.TargetDir, ".idea"), + options: options, + renameFiles: map[string]string{ + "projectname.iml": options.ProjectNameFilename + ".iml", + "gitignore.txt": ".gitignore", + "name": ".name", + }, + } + if !options.InitGit { + ideoptions.ignoredFiles = []string{"vcs.xml"} + } + err := installIDEFiles(ideoptions) + if err != nil { + return errors.Wrap(err, "generating Goland IDE files") + } + + return nil +} + +func generateVSCodeFiles(options *Options) error { + ideoptions := ideOptions{ + name: "vscode", + targetDir: filepath.Join(options.TargetDir, ".vscode"), + options: options, + } + return installIDEFiles(ideoptions) +} + +func installIDEFiles(o ideOptions) error { + source, err := debme.FS(ides, "ides/"+o.name) + if err != nil { + return err + } + + // Use gosod to install the template + installer := gosod.New(source) + + if o.renameFiles != nil { + installer.RenameFiles(o.renameFiles) + } + + for _, ignoreFile := range o.ignoredFiles { + installer.IgnoreFile(ignoreFile) + } + + binaryName := filepath.Base(o.options.TargetDir) + o.options.WindowsFlags = "" + o.options.CGOEnabled = "1" + + switch runtime.GOOS { + case "windows": + binaryName += ".exe" + o.options.WindowsFlags = " -H windowsgui" + o.options.CGOEnabled = "0" + case "darwin": + o.options.CGOLDFlags = "-framework UniformTypeIdentifiers" + } + + o.options.PathToDesktopBinary = filepath.ToSlash(filepath.Join("build", "bin", binaryName)) + + err = installer.Extract(o.targetDir, o.options) + if err != nil { + return err + } + + return nil +} diff --git a/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl b/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/lit-ts/README.md b/v2/pkg/templates/templates/lit-ts/README.md new file mode 100644 index 000000000..98d4d0447 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Lit-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/lit-ts/app.tmpl.go b/v2/pkg/templates/templates/lit-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/cmd/templates/vuebasic/.gitignore b/v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl similarity index 54% rename from cmd/templates/vuebasic/.gitignore rename to v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl index 185e66319..a547bf36d 100644 --- a/cmd/templates/vuebasic/.gitignore +++ b/v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl @@ -1,21 +1,24 @@ -.DS_Store -node_modules -/dist - -# local env files -.env.local -.env.*.local - -# Log files +# Logs +logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local # Editor directories and files +.vscode/* +!.vscode/extensions.json .idea -.vscode +.DS_Store *.suo *.ntvs* *.njsproj *.sln -*.sw* +*.sw? diff --git a/v2/pkg/templates/templates/lit-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/lit-ts/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..febcb76cb --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + + + + + + diff --git a/v2/pkg/templates/templates/lit-ts/frontend/package.json b/v2/pkg/templates/templates/lit-ts/frontend/package.json new file mode 100644 index 000000000..01aa1512c --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "dist/my-element.es.js", + "exports": { + ".": "./dist/my-element.es.js" + }, + "types": "types/my-element.d.ts", + "files": [ + "dist", + "types" + ], + "scripts": { + "dev": "vite", + "build": "tsc && vite build" + }, + "dependencies": { + "lit": "^2.2.8" + }, + "devDependencies": { + "typescript": "^4.6.4", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..b1224ec79 Binary files /dev/null and b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts b/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts new file mode 100644 index 000000000..af4e9ce20 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts @@ -0,0 +1,103 @@ +import {css, html, LitElement} from 'lit' +import logo from './assets/images/logo-universal.png' +import {Greet} from "../wailsjs/go/main/App"; +import {customElement, property} from 'lit/decorators.js' +import './style.css'; + +/** + * An example element. + * + * @slot - This element has a slot + * @csspart button - The button + */ +@customElement('my-element') +export class MyElement extends LitElement { + static styles = css` + #logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; + } + + .result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + } + + .input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; + } + + .input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; + } + + .input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; + } + + .input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + .input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + ` + + @property() + resultText = "Please enter your name below 👇" + + greet() { + let thisName = (this.shadowRoot?.getElementById('name') as HTMLInputElement)?.value; + if (thisName) { + Greet(thisName).then(result => { + this.resultText = result + }); + } + } + + render() { + return html` +
+ +
${this.resultText}
+
+ + +
+
+ ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'my-element': MyElement + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/style.css b/v2/pkg/templates/templates/lit-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json new file mode 100644 index 000000000..a28678589 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "module": "ESNext", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./types", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "Node", + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": false, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json new file mode 100644 index 000000000..b8afcc8fa --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts new file mode 100644 index 000000000..bbb7f5889 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts @@ -0,0 +1,4 @@ +import {defineConfig} from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({}) diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/lit-ts/go.mod.tmpl b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit-ts/main.go.tmpl b/v2/pkg/templates/templates/lit-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/lit-ts/template.json b/v2/pkg/templates/templates/lit-ts/template.json new file mode 100644 index 000000000..7e9beabb7 --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "Lit + Vite (Typescript)", + "shortname": "lit-ts", + "author": "Lea Anthony", + "description": "Lit + TS + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit-ts/wails.tmpl.json b/v2/pkg/templates/templates/lit-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/lit-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/lit/.gitignore.tmpl b/v2/pkg/templates/templates/lit/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/lit/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/lit/README.md b/v2/pkg/templates/templates/lit/README.md new file mode 100644 index 000000000..dc8efed65 --- /dev/null +++ b/v2/pkg/templates/templates/lit/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Lit template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/lit/app.tmpl.go b/v2/pkg/templates/templates/lit/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/lit/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl b/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/v2/pkg/templates/templates/lit/frontend/dist/gitkeep b/v2/pkg/templates/templates/lit/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/lit/frontend/index.tmpl.html b/v2/pkg/templates/templates/lit/frontend/index.tmpl.html new file mode 100644 index 000000000..fbe3eb240 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + + + + + + diff --git a/v2/pkg/templates/templates/lit/frontend/package.json b/v2/pkg/templates/templates/lit/frontend/package.json new file mode 100644 index 000000000..10c9a760e --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "dist/my-element.es.js", + "exports": { + ".": "./dist/my-element.es.js" + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "lit": "^2.2.8" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/lit/frontend/src/my-element.js b/v2/pkg/templates/templates/lit/frontend/src/my-element.js new file mode 100644 index 000000000..017632c09 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/src/my-element.js @@ -0,0 +1,106 @@ +import {css, html, LitElement} from 'lit' +import logo from './assets/images/logo-universal.png' +import {Greet} from "../wailsjs/go/main/App"; +import './style.css'; + +/** + * An example element. + * + * @slot - This element has a slot + * @csspart button - The button + */ +export class MyElement extends LitElement { + constructor() { + super() + this.resultText = "Please enter your name below 👇" + } + + static get styles() { + return css` + #logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; + } + + .result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + } + + .input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; + } + + .input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; + } + + .input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; + } + + .input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + .input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); + } + + ` + } + + static get properties() { + return { + resultText: {type: String}, + } + } + + greet() { + let thisName = this.shadowRoot.getElementById('name').value + Greet(thisName).then(result => { + this.resultText = result + }); + } + + render() { + return html` +
+ +
${this.resultText}
+
+ + +
+
+ ` + } + +} + +window.customElements.define('my-element', MyElement) diff --git a/v2/pkg/templates/templates/lit/frontend/src/style.css b/v2/pkg/templates/templates/lit/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/lit/frontend/vite.config.js b/v2/pkg/templates/templates/lit/frontend/vite.config.js new file mode 100644 index 000000000..bbb7f5889 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/vite.config.js @@ -0,0 +1,4 @@ +import {defineConfig} from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({}) diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/lit/go.mod.tmpl b/v2/pkg/templates/templates/lit/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/lit/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit/main.go.tmpl b/v2/pkg/templates/templates/lit/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/lit/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/lit/template.json b/v2/pkg/templates/templates/lit/template.json new file mode 100644 index 000000000..168769e37 --- /dev/null +++ b/v2/pkg/templates/templates/lit/template.json @@ -0,0 +1,7 @@ +{ + "name": "Lit + Vite", + "shortname": "lit", + "author": "Lea Anthony", + "description": "Lit + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/lit/wails.tmpl.json b/v2/pkg/templates/templates/lit/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/lit/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/plain/.gitignore.tmpl b/v2/pkg/templates/templates/plain/.gitignore.tmpl new file mode 100644 index 000000000..b92a6f8bf --- /dev/null +++ b/v2/pkg/templates/templates/plain/.gitignore.tmpl @@ -0,0 +1,12 @@ +# Wails bin directory +build/bin +# Wails Windows NSIS support files +build/windows/installer/wails_tools.nsh +build/windows/installer/tmp/ + +# IDEs +.idea +.vscode + +# The black hole that is... +node_modules diff --git a/v2/pkg/templates/templates/plain/README.md b/v2/pkg/templates/templates/plain/README.md new file mode 100644 index 000000000..3a71c4e9b --- /dev/null +++ b/v2/pkg/templates/templates/plain/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This template uses plain JS / HTML and CSS. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/plain/app.go b/v2/pkg/templates/templates/plain/app.go new file mode 100644 index 000000000..224be7156 --- /dev/null +++ b/v2/pkg/templates/templates/plain/app.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called at application startup +func (a *App) startup(ctx context.Context) { + // Perform your setup here + a.ctx = ctx +} + +// domReady is called after front-end resources have been loaded +func (a App) domReady(ctx context.Context) { + // Add your action here +} + +// beforeClose is called when the application is about to quit, +// either by clicking the window close button or calling runtime.Quit. +// Returning true will cause the application to continue, false will continue shutdown as normal. +func (a *App) beforeClose(ctx context.Context) (prevent bool) { + return false +} + +// shutdown is called at application termination +func (a *App) shutdown(ctx context.Context) { + // Perform your teardown here +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html b/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html new file mode 100644 index 000000000..a8a434a37 --- /dev/null +++ b/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html @@ -0,0 +1,21 @@ + + + + + + {{.ProjectName}} + + + + +
+ +
Please enter your name below 👇
+
+ + +
+
+ + + diff --git a/v2/pkg/templates/templates/plain/frontend/src/main.css b/v2/pkg/templates/templates/plain/frontend/src/main.css new file mode 100644 index 000000000..dab87d09a --- /dev/null +++ b/v2/pkg/templates/templates/plain/frontend/src/main.css @@ -0,0 +1,82 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} + +.logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-image: url("./assets/images/logo-universal.png"); + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} diff --git a/v2/pkg/templates/templates/plain/frontend/src/main.js b/v2/pkg/templates/templates/plain/frontend/src/main.js new file mode 100644 index 000000000..e4945441d --- /dev/null +++ b/v2/pkg/templates/templates/plain/frontend/src/main.js @@ -0,0 +1,33 @@ +// Get input + focus +let nameElement = document.getElementById("name"); +nameElement.focus(); +import './main.css'; + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + window.go.main.App.Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +nameElement.onkeydown = function (e) { + if (e.keyCode == 13) { + window.greet(); + } +}; diff --git a/v2/pkg/templates/templates/plain/go.mod.tmpl b/v2/pkg/templates/templates/plain/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/plain/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/plain/main.go.tmpl b/v2/pkg/templates/templates/plain/main.go.tmpl new file mode 100644 index 000000000..847803db6 --- /dev/null +++ b/v2/pkg/templates/templates/plain/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/src +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/plain/template.json b/v2/pkg/templates/templates/plain/template.json new file mode 100644 index 000000000..fc919bc3b --- /dev/null +++ b/v2/pkg/templates/templates/plain/template.json @@ -0,0 +1,7 @@ +{ + "name": "Plain HTML/JS/CSS", + "shortname": "plain", + "author": "Lea Anthony ", + "description": "A simple template using only HTML/CSS/JS", + "helpurl": "https://github.com/wailsapp/wails" +} diff --git a/v2/pkg/templates/templates/plain/wails.tmpl.json b/v2/pkg/templates/templates/plain/wails.tmpl.json new file mode 100644 index 000000000..0168826bd --- /dev/null +++ b/v2/pkg/templates/templates/plain/wails.tmpl.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "wailsjsdir": "./frontend", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/preact-ts/.gitignore.tmpl b/v2/pkg/templates/templates/preact-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/preact-ts/README.md b/v2/pkg/templates/templates/preact-ts/README.md new file mode 100644 index 000000000..923ecb002 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Preact-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/preact-ts/app.tmpl.go b/v2/pkg/templates/templates/preact-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/preact-ts/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/preact-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/preact-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..0fb692c96 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/templates/preact-ts/frontend/package.json b/v2/pkg/templates/templates/preact-ts/frontend/package.json new file mode 100644 index 000000000..014e8df79 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "^10.10.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.3.0", + "typescript": "^4.6.4", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/App.css b/v2/pkg/templates/templates/preact-ts/frontend/src/App.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/App.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/app.tsx b/v2/pkg/templates/templates/preact-ts/frontend/src/app.tsx new file mode 100644 index 000000000..9875320f9 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/app.tsx @@ -0,0 +1,30 @@ +import './App.css' +import logo from "./assets/images/logo-universal.png" +import {Greet} from "../wailsjs/go/main/App"; +import {useState} from "preact/hooks"; +import {h} from 'preact'; + +export function App(props: any) { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e: any) => setName(e.target.value); + const updateResultText = (result: string) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( + <> +
+ +
{resultText}
+
+ + +
+
+ + ) +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/pkg/templates/templates/preact-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/main.tsx b/v2/pkg/templates/templates/preact-ts/frontend/src/main.tsx new file mode 100644 index 000000000..05b147282 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/main.tsx @@ -0,0 +1,5 @@ +import {render} from 'preact'; +import {App} from './app'; +import './style.css'; + +render(, document.getElementById('app')!); diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/preact.d.ts b/v2/pkg/templates/templates/preact-ts/frontend/src/preact.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/style.css b/v2/pkg/templates/templates/preact-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/preact-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.json new file mode 100644 index 000000000..d6f1807ef --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.node.json new file mode 100644 index 000000000..b8afcc8fa --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/preact-ts/frontend/vite.config.ts new file mode 100644 index 000000000..25845ba4b --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()] +}) diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/preact-ts/go.mod.tmpl b/v2/pkg/templates/templates/preact-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact-ts/main.go.tmpl b/v2/pkg/templates/templates/preact-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/preact-ts/template.json b/v2/pkg/templates/templates/preact-ts/template.json new file mode 100644 index 000000000..b7b46a64c --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "Preact + Vite (Typescript)", + "shortname": "preact-ts", + "author": "Lea Anthony", + "description": "Preact + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact-ts/wails.tmpl.json b/v2/pkg/templates/templates/preact-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/preact-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/preact/.gitignore.tmpl b/v2/pkg/templates/templates/preact/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/preact/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/preact/README.md b/v2/pkg/templates/templates/preact/README.md new file mode 100644 index 000000000..8a354d53b --- /dev/null +++ b/v2/pkg/templates/templates/preact/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Preact template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/preact/app.tmpl.go b/v2/pkg/templates/templates/preact/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/preact/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/preact/frontend/dist/gitkeep b/v2/pkg/templates/templates/preact/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/preact/frontend/index.tmpl.html b/v2/pkg/templates/templates/preact/frontend/index.tmpl.html new file mode 100644 index 000000000..c8bfd4b76 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/templates/preact/frontend/package.json b/v2/pkg/templates/templates/preact/frontend/package.json new file mode 100644 index 000000000..f8d09a99d --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "^10.10.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.3.0", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/frontend/src/app.css b/v2/pkg/templates/templates/preact/frontend/src/app.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/app.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/frontend/src/app.jsx b/v2/pkg/templates/templates/preact/frontend/src/app.jsx new file mode 100644 index 000000000..d0543d081 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/app.jsx @@ -0,0 +1,29 @@ +import './app.css'; +import logo from "./assets/images/logo-universal.png"; +import {Greet} from "../wailsjs/go/main/App"; +import {useState} from "preact/hooks"; + +export function App(props) { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e) => setName(e.target.value); + const updateResultText = (result) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( + <> +
+ +
{resultText}
+
+ + +
+
+ + ) +} diff --git a/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/preact/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/preact/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/pkg/templates/templates/preact/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg b/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg new file mode 100644 index 000000000..23433fcf8 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/frontend/src/main.jsx b/v2/pkg/templates/templates/preact/frontend/src/main.jsx new file mode 100644 index 000000000..6c42a5949 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/main.jsx @@ -0,0 +1,5 @@ +import {render} from 'preact'; +import {App} from './app'; +import './style.css'; + +render(, document.getElementById('app')); \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/frontend/src/style.css b/v2/pkg/templates/templates/preact/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/preact/frontend/vite.config.js b/v2/pkg/templates/templates/preact/frontend/vite.config.js new file mode 100644 index 000000000..25845ba4b --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()] +}) diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/preact/go.mod.tmpl b/v2/pkg/templates/templates/preact/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/preact/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/main.go.tmpl b/v2/pkg/templates/templates/preact/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/preact/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/preact/template.json b/v2/pkg/templates/templates/preact/template.json new file mode 100644 index 000000000..034c37478 --- /dev/null +++ b/v2/pkg/templates/templates/preact/template.json @@ -0,0 +1,7 @@ +{ + "name": "Preact + Vite", + "shortname": "preact", + "author": "Lea Anthony", + "description": "Preact + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/wails.tmpl.json b/v2/pkg/templates/templates/preact/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/preact/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/react-ts/.gitignore.tmpl b/v2/pkg/templates/templates/react-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/react-ts/README.md b/v2/pkg/templates/templates/react-ts/README.md new file mode 100644 index 000000000..d2169cc44 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails React-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/react-ts/app.tmpl.go b/v2/pkg/templates/templates/react-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/react-ts/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/react-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/react-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..a2023cac7 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/templates/react-ts/frontend/package.json b/v2/pkg/templates/templates/react-ts/frontend/package.json new file mode 100644 index 000000000..f0106ca9a --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^2.0.1", + "typescript": "^4.6.4", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/App.css b/v2/pkg/templates/templates/react-ts/frontend/src/App.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/App.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/App.tsx b/v2/pkg/templates/templates/react-ts/frontend/src/App.tsx new file mode 100644 index 000000000..a6e56f9f8 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/App.tsx @@ -0,0 +1,28 @@ +import {useState} from 'react'; +import logo from './assets/images/logo-universal.png'; +import './App.css'; +import {Greet} from "../wailsjs/go/main/App"; + +function App() { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e: any) => setName(e.target.value); + const updateResultText = (result: string) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( +
+ +
{resultText}
+
+ + +
+
+ ) +} + +export default App diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/react-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/react-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..99ac71f5a Binary files /dev/null and b/v2/pkg/templates/templates/react-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/main.tsx b/v2/pkg/templates/templates/react-ts/frontend/src/main.tsx new file mode 100644 index 000000000..3626ff303 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import {createRoot} from 'react-dom/client' +import './style.css' +import App from './App' + +const container = document.getElementById('root') + +const root = createRoot(container!) + +root.render( + + + +) diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/style.css b/v2/pkg/templates/templates/react-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/react-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v2/pkg/templates/templates/react-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/react-ts/frontend/tsconfig.json new file mode 100644 index 000000000..823e83d11 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/react-ts/frontend/tsconfig.node.json new file mode 100644 index 000000000..b8afcc8fa --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/react-ts/frontend/vite.config.ts new file mode 100644 index 000000000..49550655a --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}) diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/react-ts/go.mod.tmpl b/v2/pkg/templates/templates/react-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react-ts/main.go.tmpl b/v2/pkg/templates/templates/react-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/react-ts/template.json b/v2/pkg/templates/templates/react-ts/template.json new file mode 100644 index 000000000..7e9753770 --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "React + Vite (Typescript)", + "shortname": "react-ts", + "author": "Lea Anthony", + "description": "React + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react-ts/wails.tmpl.json b/v2/pkg/templates/templates/react-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/react-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/react/.gitignore.tmpl b/v2/pkg/templates/templates/react/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/react/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/react/README.md b/v2/pkg/templates/templates/react/README.md new file mode 100644 index 000000000..4db88f690 --- /dev/null +++ b/v2/pkg/templates/templates/react/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails React template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/react/app.tmpl.go b/v2/pkg/templates/templates/react/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/react/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/react/frontend/dist/gitkeep b/v2/pkg/templates/templates/react/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/react/frontend/index.tmpl.html b/v2/pkg/templates/templates/react/frontend/index.tmpl.html new file mode 100644 index 000000000..80aa30b89 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/index.tmpl.html @@ -0,0 +1,13 @@ + + + + + + {{.ProjectName}} + + +
+ + + + diff --git a/v2/pkg/templates/templates/react/frontend/package.json b/v2/pkg/templates/templates/react/frontend/package.json new file mode 100644 index 000000000..3b34e1f8f --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^2.0.1", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react/frontend/src/App.css b/v2/pkg/templates/templates/react/frontend/src/App.css new file mode 100644 index 000000000..f949d9c18 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/src/App.css @@ -0,0 +1,59 @@ +#app { + height: 100vh; + text-align: center; +} + +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react/frontend/src/App.jsx b/v2/pkg/templates/templates/react/frontend/src/App.jsx new file mode 100644 index 000000000..fd762291f --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/src/App.jsx @@ -0,0 +1,28 @@ +import {useState} from 'react'; +import logo from './assets/images/logo-universal.png'; +import './App.css'; +import {Greet} from "../wailsjs/go/main/App"; + +function App() { + const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [name, setName] = useState(''); + const updateName = (e) => setName(e.target.value); + const updateResultText = (result) => setResultText(result); + + function greet() { + Greet(name).then(updateResultText); + } + + return ( +
+ +
{resultText}
+
+ + +
+
+ ) +} + +export default App diff --git a/v2/pkg/templates/templates/react/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/react/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/react/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/react/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..5421ad881 Binary files /dev/null and b/v2/pkg/templates/templates/react/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/react/frontend/src/main.jsx b/v2/pkg/templates/templates/react/frontend/src/main.jsx new file mode 100644 index 000000000..e50e105db --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/src/main.jsx @@ -0,0 +1,14 @@ +import React from 'react' +import {createRoot} from 'react-dom/client' +import './style.css' +import App from './App' + +const container = document.getElementById('root') + +const root = createRoot(container) + +root.render( + + + +) diff --git a/v2/pkg/templates/templates/react/frontend/src/style.css b/v2/pkg/templates/templates/react/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/react/frontend/vite.config.js b/v2/pkg/templates/templates/react/frontend/vite.config.js new file mode 100644 index 000000000..49550655a --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}) diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/react/go.mod.tmpl b/v2/pkg/templates/templates/react/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/react/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react/main.go.tmpl b/v2/pkg/templates/templates/react/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/react/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/react/template.json b/v2/pkg/templates/templates/react/template.json new file mode 100644 index 000000000..eb6de08f3 --- /dev/null +++ b/v2/pkg/templates/templates/react/template.json @@ -0,0 +1,7 @@ +{ + "name": "React + Vite", + "shortname": "react", + "author": "Lea Anthony", + "description": "React + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/react/wails.tmpl.json b/v2/pkg/templates/templates/react/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/react/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/svelte-ts/.gitignore.tmpl b/v2/pkg/templates/templates/svelte-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/svelte-ts/README.md b/v2/pkg/templates/templates/svelte-ts/README.md new file mode 100644 index 000000000..2e62a374f --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/README.md @@ -0,0 +1,16 @@ +# README + +## About + +This is the official Wails Svelte-TS template. + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/svelte-ts/app.tmpl.go b/v2/pkg/templates/templates/svelte-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/.vscode/extensions.json b/v2/pkg/templates/templates/svelte-ts/frontend/.vscode/extensions.json new file mode 100644 index 000000000..b869ef8e2 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "svelte.svelte-vscode" + ] +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/README.md b/v2/pkg/templates/templates/svelte-ts/frontend/README.md new file mode 100644 index 000000000..bd0780d0a --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/README.md @@ -0,0 +1,65 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + ++ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its +serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, +and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account +the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the +other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte +project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been +structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash +references keeps the default TypeScript setting of accepting type information from the entire workspace, while also +adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to +install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of +JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: +not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing +JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` +and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the +details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be +replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/svelte-ts/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/svelte-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..3dd212f2d --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/package.json b/v2/pkg/templates/templates/svelte-ts/frontend/package.json new file mode 100644 index 000000000..2ee69eaf5 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "@tsconfig/svelte": "^3.0.0", + "svelte": "^3.49.0", + "svelte-check": "^2.8.0", + "svelte-preprocess": "^4.10.7", + "tslib": "^2.4.0", + "typescript": "^4.6.4", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/App.svelte b/v2/pkg/templates/templates/svelte-ts/frontend/src/App.svelte new file mode 100644 index 000000000..1987eb090 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/src/App.svelte @@ -0,0 +1,79 @@ + + +
+ +
{resultText}
+
+ + +
+
+ + diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..5421ad881 Binary files /dev/null and b/v2/pkg/templates/templates/svelte-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/main.ts b/v2/pkg/templates/templates/svelte-ts/frontend/src/main.ts new file mode 100644 index 000000000..95c41a51d --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/src/main.ts @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/style.css b/v2/pkg/templates/templates/svelte-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/svelte-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/svelte.config.js b/v2/pkg/templates/templates/svelte-ts/frontend/svelte.config.js new file mode 100644 index 000000000..3630bb396 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/svelte.config.js @@ -0,0 +1,7 @@ +import sveltePreprocess from 'svelte-preprocess' + +export default { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: sveltePreprocess() +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.json new file mode 100644 index 000000000..2cffdc568 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.ts", + "src/**/*.js", + "src/**/*.svelte" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.node.json new file mode 100644 index 000000000..05764b1c4 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node" + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/svelte-ts/frontend/vite.config.ts new file mode 100644 index 000000000..d37616f9a --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import {svelte} from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()] +}) diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl b/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte-ts/main.go.tmpl b/v2/pkg/templates/templates/svelte-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/svelte-ts/template.json b/v2/pkg/templates/templates/svelte-ts/template.json new file mode 100644 index 000000000..eec762bc6 --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "Svelte + Vite (Typescript)", + "shortname": "svelte-ts", + "author": "Lea Anthony", + "description": "Svelte + TS + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte-ts/wails.tmpl.json b/v2/pkg/templates/templates/svelte-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/svelte-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/svelte/.gitignore.tmpl b/v2/pkg/templates/templates/svelte/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/svelte/README.md b/v2/pkg/templates/templates/svelte/README.md new file mode 100644 index 000000000..eefcd5c4e --- /dev/null +++ b/v2/pkg/templates/templates/svelte/README.md @@ -0,0 +1,16 @@ +# README + +## About + +This is the official Wails Svelte template. + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/svelte/app.tmpl.go b/v2/pkg/templates/templates/svelte/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/svelte/frontend/.vscode/extensions.json b/v2/pkg/templates/templates/svelte/frontend/.vscode/extensions.json new file mode 100644 index 000000000..b869ef8e2 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "svelte.svelte-vscode" + ] +} diff --git a/v2/pkg/templates/templates/svelte/frontend/README.md b/v2/pkg/templates/templates/svelte/frontend/README.md new file mode 100644 index 000000000..a346289c5 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/README.md @@ -0,0 +1,63 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + ++ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its +serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, +and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer +experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` +templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been +structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash +references keeps the default TypeScript setting of accepting type information from the entire workspace, while also +adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to +install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. +This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of +JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` +and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the +details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be +replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/v2/pkg/templates/templates/svelte/frontend/dist/gitkeep b/v2/pkg/templates/templates/svelte/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/svelte/frontend/index.tmpl.html b/v2/pkg/templates/templates/svelte/frontend/index.tmpl.html new file mode 100644 index 000000000..859919153 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/templates/svelte/frontend/jsconfig.json b/v2/pkg/templates/templates/svelte/frontend/jsconfig.json new file mode 100644 index 000000000..3918b4fda --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/jsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": [ + "src/**/*.d.ts", + "src/**/*.js", + "src/**/*.svelte" + ] +} diff --git a/v2/pkg/templates/templates/svelte/frontend/package.json b/v2/pkg/templates/templates/svelte/frontend/package.json new file mode 100644 index 000000000..8c9ae62a8 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/package.json @@ -0,0 +1,16 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte/frontend/src/App.svelte b/v2/pkg/templates/templates/svelte/frontend/src/App.svelte new file mode 100644 index 000000000..2a2ce2282 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/src/App.svelte @@ -0,0 +1,79 @@ + + +
+ +
{resultText}
+
+ + +
+
+ + diff --git a/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/svelte/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/svelte/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/svelte/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/pkg/templates/templates/svelte/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/svelte/frontend/src/main.js b/v2/pkg/templates/templates/svelte/frontend/src/main.js new file mode 100644 index 000000000..95c41a51d --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/src/main.js @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/v2/pkg/templates/templates/svelte/frontend/src/style.css b/v2/pkg/templates/templates/svelte/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/svelte/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/svelte/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/v2/pkg/templates/templates/svelte/frontend/vite.config.js b/v2/pkg/templates/templates/svelte/frontend/vite.config.js new file mode 100644 index 000000000..d37616f9a --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import {svelte} from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()] +}) diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/svelte/go.mod.tmpl b/v2/pkg/templates/templates/svelte/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte/main.go.tmpl b/v2/pkg/templates/templates/svelte/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/svelte/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/svelte/template.json b/v2/pkg/templates/templates/svelte/template.json new file mode 100644 index 000000000..fb02c7a8b --- /dev/null +++ b/v2/pkg/templates/templates/svelte/template.json @@ -0,0 +1,7 @@ +{ + "name": "Svelte + Vite", + "shortname": "svelte", + "author": "Lea Anthony", + "description": "Svelte + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte/wails.tmpl.json b/v2/pkg/templates/templates/svelte/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/svelte/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/vanilla-ts/.gitignore.tmpl b/v2/pkg/templates/templates/vanilla-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/vanilla-ts/README.md b/v2/pkg/templates/templates/vanilla-ts/README.md new file mode 100644 index 000000000..4d7bcd378 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/vanilla-ts/app.tmpl.go b/v2/pkg/templates/templates/vanilla-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/vanilla-ts/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/vanilla-ts/frontend/index.tmpl.html new file mode 100644 index 000000000..3dd212f2d --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/package.json b/v2/pkg/templates/templates/vanilla-ts/frontend/package.json new file mode 100644 index 000000000..c57eb8610 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^4.5.4", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/app.css b/v2/pkg/templates/templates/vanilla-ts/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/pkg/templates/templates/vanilla-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/main.ts b/v2/pkg/templates/templates/vanilla-ts/frontend/src/main.ts new file mode 100644 index 000000000..b68d7d961 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/src/main.ts @@ -0,0 +1,49 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement!.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement!.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +document.querySelector('#app')!.innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +(document.getElementById('logo') as HTMLImageElement).src = logo; + +let nameElement = (document.getElementById("name") as HTMLInputElement); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +declare global { + interface Window { + greet: () => void; + } +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/style.css b/v2/pkg/templates/templates/vanilla-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/vanilla-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/vanilla-ts/frontend/tsconfig.json new file mode 100644 index 000000000..62645742d --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ESNext", + "DOM" + ], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": [ + "src" + ] +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl b/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla-ts/main.go.tmpl b/v2/pkg/templates/templates/vanilla-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/vanilla-ts/template.json b/v2/pkg/templates/templates/vanilla-ts/template.json new file mode 100644 index 000000000..5aed52c89 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "Vanilla + Vite (Typescript)", + "shortname": "vanilla-ts", + "author": "Lea Anthony", + "description": "Vanilla + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla-ts/wails.tmpl.json b/v2/pkg/templates/templates/vanilla-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/vanilla-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/vanilla/.gitignore.tmpl b/v2/pkg/templates/templates/vanilla/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/vanilla/README.md b/v2/pkg/templates/templates/vanilla/README.md new file mode 100644 index 000000000..397b08b92 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/vanilla/app.tmpl.go b/v2/pkg/templates/templates/vanilla/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/vanilla/frontend/dist/gitkeep b/v2/pkg/templates/templates/vanilla/frontend/dist/gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/v2/pkg/templates/templates/vanilla/frontend/index.tmpl.html b/v2/pkg/templates/templates/vanilla/frontend/index.tmpl.html new file mode 100644 index 000000000..859919153 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/index.tmpl.html @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/v2/pkg/templates/templates/vanilla/frontend/package.json b/v2/pkg/templates/templates/vanilla/frontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/app.css b/v2/pkg/templates/templates/vanilla/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/vanilla/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/vanilla/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/pkg/templates/templates/vanilla/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/main.js b/v2/pkg/templates/templates/vanilla/frontend/src/main.js new file mode 100644 index 000000000..4ad5a2cae --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/src/main.js @@ -0,0 +1,43 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; diff --git a/v2/pkg/templates/templates/vanilla/frontend/src/style.css b/v2/pkg/templates/templates/vanilla/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/vanilla/go.mod.tmpl b/v2/pkg/templates/templates/vanilla/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla/main.go.tmpl b/v2/pkg/templates/templates/vanilla/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/vanilla/template.json b/v2/pkg/templates/templates/vanilla/template.json new file mode 100644 index 000000000..8153663b0 --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/template.json @@ -0,0 +1,7 @@ +{ + "name": "Vanilla + Vite", + "shortname": "vanilla", + "author": "Lea Anthony", + "description": "Vanilla + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla/wails.tmpl.json b/v2/pkg/templates/templates/vanilla/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/vanilla/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/vue-ts/.gitignore.tmpl b/v2/pkg/templates/templates/vue-ts/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/vue-ts/README.md b/v2/pkg/templates/templates/vue-ts/README.md new file mode 100644 index 000000000..f0eaef091 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vue-TS template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/vue-ts/app.tmpl.go b/v2/pkg/templates/templates/vue-ts/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/README.md b/v2/pkg/templates/templates/vue-ts/frontend/README.md new file mode 100644 index 000000000..98f4a52ae --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/README.md @@ -0,0 +1,23 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue +3 ` + + + diff --git a/v2/pkg/templates/templates/vue-ts/frontend/package.json b/v2/pkg/templates/templates/vue-ts/frontend/package.json new file mode 100644 index 000000000..e65d0eff4 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.2.37" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.0.3", + "typescript": "^4.6.4", + "vite": "^3.0.7", + "vue-tsc": "^1.8.27", + "@babel/types": "^7.18.10" + } +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/App.vue b/v2/pkg/templates/templates/vue-ts/frontend/src/App.vue new file mode 100644 index 000000000..b63d187c5 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/pkg/templates/templates/vue-ts/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/templates/vue-ts/frontend/src/components/HelloWorld.vue new file mode 100644 index 000000000..3ab3df798 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/components/HelloWorld.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/main.ts b/v2/pkg/templates/templates/vue-ts/frontend/src/main.ts new file mode 100644 index 000000000..f9754fe19 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/main.ts @@ -0,0 +1,5 @@ +import {createApp} from 'vue' +import App from './App.vue' +import './style.css'; + +createApp(App).mount('#app') diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/style.css b/v2/pkg/templates/templates/vue-ts/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/vue-ts/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..dcfaef436 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type {DefineComponent} from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.json new file mode 100644 index 000000000..3cc844d92 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": [ + "ESNext", + "DOM" + ], + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.node.json new file mode 100644 index 000000000..b8afcc8fa --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/vue-ts/frontend/vite.config.ts new file mode 100644 index 000000000..a30c338ed --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/vue-ts/go.mod.tmpl b/v2/pkg/templates/templates/vue-ts/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue-ts/main.go.tmpl b/v2/pkg/templates/templates/vue-ts/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/vue-ts/template.json b/v2/pkg/templates/templates/vue-ts/template.json new file mode 100644 index 000000000..6efc20293 --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/template.json @@ -0,0 +1,7 @@ +{ + "name": "Vue + Vite (Typescript)", + "shortname": "vue-ts", + "author": "Lea Anthony", + "description": "Vue + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue-ts/wails.tmpl.json b/v2/pkg/templates/templates/vue-ts/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/vue-ts/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates/vue/.gitignore.tmpl b/v2/pkg/templates/templates/vue/.gitignore.tmpl new file mode 100644 index 000000000..129d52294 --- /dev/null +++ b/v2/pkg/templates/templates/vue/.gitignore.tmpl @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/v2/pkg/templates/templates/vue/README.md b/v2/pkg/templates/templates/vue/README.md new file mode 100644 index 000000000..d27aaeecb --- /dev/null +++ b/v2/pkg/templates/templates/vue/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vue template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/pkg/templates/templates/vue/app.tmpl.go b/v2/pkg/templates/templates/vue/app.tmpl.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/pkg/templates/templates/vue/app.tmpl.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/pkg/templates/templates/vue/frontend/README.md b/v2/pkg/templates/templates/vue/frontend/README.md new file mode 100644 index 000000000..b4719bec0 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/README.md @@ -0,0 +1,8 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + + diff --git a/v2/pkg/templates/templates/vue/frontend/package.json b/v2/pkg/templates/templates/vue/frontend/package.json new file mode 100644 index 000000000..43da9cbd8 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.2.37" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.0.3", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue/frontend/src/App.vue b/v2/pkg/templates/templates/vue/frontend/src/App.vue new file mode 100644 index 000000000..15d2f1215 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/pkg/templates/templates/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/pkg/templates/templates/vue/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/vue/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..e9913c1d4 Binary files /dev/null and b/v2/pkg/templates/templates/vue/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/pkg/templates/templates/vue/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/templates/vue/frontend/src/components/HelloWorld.vue new file mode 100644 index 000000000..29c023fbe --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/src/components/HelloWorld.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/v2/pkg/templates/templates/vue/frontend/src/main.js b/v2/pkg/templates/templates/vue/frontend/src/main.js new file mode 100644 index 000000000..f9754fe19 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/src/main.js @@ -0,0 +1,5 @@ +import {createApp} from 'vue' +import App from './App.vue' +import './style.css'; + +createApp(App).mount('#app') diff --git a/v2/pkg/templates/templates/vue/frontend/src/style.css b/v2/pkg/templates/templates/vue/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/pkg/templates/templates/vue/frontend/vite.config.js b/v2/pkg/templates/templates/vue/frontend/vite.config.js new file mode 100644 index 000000000..a30c338ed --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..43173cfce --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1: string): Promise; diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..0ee085c95 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..336fb07aa --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,211 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width: number + height: number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..b5ae16d56 --- /dev/null +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,182 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName) { + return window.runtime.EventsOff(eventName); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} diff --git a/v2/pkg/templates/templates/vue/go.mod.tmpl b/v2/pkg/templates/templates/vue/go.mod.tmpl new file mode 100644 index 000000000..4b34d1668 --- /dev/null +++ b/v2/pkg/templates/templates/vue/go.mod.tmpl @@ -0,0 +1,7 @@ +module changeme + +go 1.23.0 + +require github.com/wailsapp/wails/v2 {{.WailsVersion}} + +// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue/main.go.tmpl b/v2/pkg/templates/templates/vue/main.go.tmpl new file mode 100644 index 000000000..e24782be3 --- /dev/null +++ b/v2/pkg/templates/templates/vue/main.go.tmpl @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "{{.ProjectName}}", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/pkg/templates/templates/vue/template.json b/v2/pkg/templates/templates/vue/template.json new file mode 100644 index 000000000..c2529e353 --- /dev/null +++ b/v2/pkg/templates/templates/vue/template.json @@ -0,0 +1,7 @@ +{ + "name": "Vue + Vite", + "shortname": "vue", + "author": "Lea Anthony", + "description": "Vue + Vite development server", + "helpurl": "https://wails.io" +} \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue/wails.tmpl.json b/v2/pkg/templates/templates/vue/wails.tmpl.json new file mode 100644 index 000000000..c39b2cb7d --- /dev/null +++ b/v2/pkg/templates/templates/vue/wails.tmpl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "{{.ProjectName}}", + "outputfilename": "{{.BinaryName}}", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "{{.AuthorName}}", + "email": "{{.AuthorEmail}}" + } +} diff --git a/v2/pkg/templates/templates_test.go b/v2/pkg/templates/templates_test.go new file mode 100644 index 000000000..658ecadb6 --- /dev/null +++ b/v2/pkg/templates/templates_test.go @@ -0,0 +1,99 @@ +package templates + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/matryer/is" +) + +func TestList(t *testing.T) { + + is2 := is.New(t) + templateList, err := List() + is2.NoErr(err) + + is2.Equal(len(templateList), 13) +} + +func TestShortname(t *testing.T) { + + is2 := is.New(t) + + vanillaTemplate, err := getTemplateByShortname("vanilla") + is2.NoErr(err) + + is2.Equal(vanillaTemplate.Name, "Vanilla + Vite") +} + +func TestInstall(t *testing.T) { + + is2 := is.New(t) + + // Change to the directory of this file + _, filename, _, _ := runtime.Caller(0) + + err := os.Chdir(filepath.Dir(filename)) + is2.NoErr(err) + + options := &Options{ + ProjectName: "test", + TemplateName: "vanilla", + AuthorName: "Lea Anthony", + AuthorEmail: "lea.anthony@gmail.com", + } + + defer func() { + _ = os.RemoveAll(options.ProjectName) + }() + _, _, err = Install(options) + is2.NoErr(err) + +} + +func TestInstallFailsInNonEmptyDirectory(t *testing.T) { + is2 := is.New(t) + + // Create a temp directory with a file in it + tempDir, err := os.MkdirTemp("", "wails-test-nonempty-*") + is2.NoErr(err) + defer func() { + _ = os.RemoveAll(tempDir) + }() + + // Create a file in the directory to make it non-empty + err = os.WriteFile(filepath.Join(tempDir, "existing-file.txt"), []byte("test"), 0644) + is2.NoErr(err) + + options := &Options{ + ProjectName: "test", + TemplateName: "vanilla", + TargetDir: tempDir, + } + + _, _, err = Install(options) + is2.True(err != nil) // Should fail + is2.True(err.Error() == "cannot initialise project in non-empty directory: "+tempDir) +} + +func TestInstallSucceedsInEmptyDirectory(t *testing.T) { + is2 := is.New(t) + + // Create an empty temp directory + tempDir, err := os.MkdirTemp("", "wails-test-empty-*") + is2.NoErr(err) + defer func() { + _ = os.RemoveAll(tempDir) + }() + + options := &Options{ + ProjectName: "test", + TemplateName: "vanilla", + TargetDir: tempDir, + } + + _, _, err = Install(options) + is2.NoErr(err) // Should succeed in empty directory +} diff --git a/v2/tools/release/release.go b/v2/tools/release/release.go new file mode 100644 index 000000000..4178fcc95 --- /dev/null +++ b/v2/tools/release/release.go @@ -0,0 +1,127 @@ +package main + +import ( + "encoding/json" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/wailsapp/wails/v2/internal/s" +) + +const versionFile = "../../cmd/wails/internal/version.txt" + +func checkError(err error) { + if err != nil { + println(err.Error()) + os.Exit(1) + } +} + +// TODO:This can be replaced with "https://github.com/coreos/go-semver/blob/main/semver/semver.go" +func updateVersion() string { + currentVersionData, err := os.ReadFile(versionFile) + checkError(err) + currentVersion := string(currentVersionData) + vsplit := strings.Split(currentVersion, ".") + minorVersion, err := strconv.Atoi(vsplit[len(vsplit)-1]) + checkError(err) + minorVersion++ + vsplit[len(vsplit)-1] = strconv.Itoa(minorVersion) + newVersion := strings.Join(vsplit, ".") + err = os.WriteFile(versionFile, []byte(newVersion), 0o755) + checkError(err) + return newVersion +} + +func runCommand(name string, arg ...string) { + cmd := exec.Command(name, arg...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + checkError(err) +} + +func IsPointRelease(currentVersion string, newVersion string) bool { + // The first n-1 parts of the version should be the same + if currentVersion[:len(currentVersion)-2] != newVersion[:len(newVersion)-2] { + return false + } + // split on the last dot in the string + currentVersionSplit := strings.Split(currentVersion, ".") + newVersionSplit := strings.Split(newVersion, ".") + // compare the + // if the last part of the version is the same, it's a point release + currentMinor := lo.Must(strconv.Atoi(currentVersionSplit[len(currentVersionSplit)-1])) + newMinor := lo.Must(strconv.Atoi(newVersionSplit[len(newVersionSplit)-1])) + return newMinor == currentMinor+1 +} + +func main() { + var newVersion string + var isPointRelease bool + if len(os.Args) > 1 { + newVersion = os.Args[1] + currentVersion, err := os.ReadFile(versionFile) + checkError(err) + err = os.WriteFile(versionFile, []byte(newVersion), 0o755) + checkError(err) + isPointRelease = IsPointRelease(string(currentVersion), newVersion) + } else { + newVersion = updateVersion() + } + + // Update ChangeLog + s.CD("../../../website") + + // Read in `src/pages/changelog.mdx` + changelogData, err := os.ReadFile("src/pages/changelog.mdx") + checkError(err) + changelog := string(changelogData) + // Split on the line that has `## [Unreleased]` + changelogSplit := strings.Split(changelog, "## [Unreleased]") + // Get today's date in YYYY-MM-DD format + today := time.Now().Format("2006-01-02") + // Add the new version to the top of the changelog + newChangelog := changelogSplit[0] + "## [Unreleased]\n\n## " + newVersion + " - " + today + changelogSplit[1] + // Write the changelog back + err = os.WriteFile("src/pages/changelog.mdx", []byte(newChangelog), 0o755) + checkError(err) + + if !isPointRelease { + runCommand("npx", "-y", "pnpm", "install") + + s.ECHO("Generating new Docs for version: " + newVersion) + + runCommand("npx", "pnpm", "run", "docusaurus", "docs:version", newVersion) + + runCommand("npx", "pnpm", "run", "write-translations") + + // Load the version list/* + versionsData, err := os.ReadFile("versions.json") + checkError(err) + var versions []string + err = json.Unmarshal(versionsData, &versions) + checkError(err) + oldestVersion := versions[len(versions)-1] + s.ECHO(oldestVersion) + versions = versions[0 : len(versions)-1] + newVersions, err := json.Marshal(&versions) + checkError(err) + err = os.WriteFile("versions.json", newVersions, 0o755) + checkError(err) + + s.ECHO("Removing old version: " + oldestVersion) + s.CD("versioned_docs") + s.RMDIR("version-" + oldestVersion) + s.CD("../versioned_sidebars") + s.RM("version-" + oldestVersion + "-sidebars.json") + s.CD("..") + + runCommand("npx", "pnpm", "run", "build") + } +} diff --git a/v2/wails.go b/v2/wails.go new file mode 100644 index 000000000..54be64ca2 --- /dev/null +++ b/v2/wails.go @@ -0,0 +1,15 @@ +// Package wails is the main package of the Wails project. +// It is used by client applications. +package wails + +import ( + _ "github.com/wailsapp/wails/v2/internal/goversion" // Add Compile-Time version check for minimum go version + "github.com/wailsapp/wails/v2/pkg/application" + "github.com/wailsapp/wails/v2/pkg/options" +) + +// Run creates an application based on the given config and executes it +func Run(options *options.App) error { + mainApp := application.NewWithOptions(options) + return mainApp.Run() +} diff --git a/v3/scripts/validate-changelog.go b/v3/scripts/validate-changelog.go new file mode 100644 index 000000000..659285a20 --- /dev/null +++ b/v3/scripts/validate-changelog.go @@ -0,0 +1,270 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +func main() { + if len(os.Args) < 3 { + fmt.Println("Usage: go run validate-changelog.go ") + os.Exit(1) + } + + changelogPath := os.Args[1] + addedLinesPath := os.Args[2] + + // Read changelog + content, err := readFile(changelogPath) + if err != nil { + fmt.Printf("ERROR: Failed to read changelog: %v\n", err) + os.Exit(1) + } + + // Read the lines added in this PR + addedContent, err := readFile(addedLinesPath) + if err != nil { + fmt.Printf("ERROR: Failed to read PR added lines: %v\n", err) + os.Exit(1) + } + + addedLines := strings.Split(addedContent, "\n") + fmt.Printf("📝 Lines added in this PR: %d\n", len(addedLines)) + + // Parse changelog to find where added lines ended up + lines := strings.Split(content, "\n") + + // Find problematic entries - only check lines that were ADDED in this PR + var issues []Issue + currentSection := "" + + for lineNum, line := range lines { + // Track current section + if strings.HasPrefix(line, "## ") { + if strings.Contains(line, "[Unreleased]") { + currentSection = "Unreleased" + } else if strings.Contains(line, "v3.0.0-alpha") { + // Extract version from line like "## v3.0.0-alpha.10 - 2025-07-06" + parts := strings.Split(strings.TrimSpace(line[3:]), " - ") + if len(parts) >= 1 { + currentSection = strings.TrimSpace(parts[0]) + } + } + } + + // Check if this line was added in this PR AND is in a released version + if currentSection != "" && currentSection != "Unreleased" && + strings.HasPrefix(strings.TrimSpace(line), "- ") && + wasAddedInThisPR(line, addedLines) { + + issues = append(issues, Issue{ + Line: lineNum, + Content: strings.TrimSpace(line), + Section: currentSection, + Category: getCurrentCategory(lines, lineNum), + }) + fmt.Printf("🚨 MISPLACED: Line added to released version %s: %s\n", currentSection, strings.TrimSpace(line)) + } + } + + if len(issues) == 0 { + fmt.Println("VALIDATION_RESULT=success") + fmt.Println("No misplaced changelog entries found ✅") + return + } + + // Try to fix the issues + fmt.Printf("Found %d potentially misplaced entries:\n", len(issues)) + for _, issue := range issues { + fmt.Printf(" - Line %d in %s: %s\n", issue.Line+1, issue.Section, issue.Content) + } + + // Attempt automatic fix + fixed, err := attemptFix(content, issues, changelogPath) + if err != nil { + fmt.Printf("VALIDATION_RESULT=error\n") + fmt.Printf("ERROR: Failed to fix changelog: %v\n", err) + os.Exit(1) + } + + if fixed { + fmt.Println("VALIDATION_RESULT=fixed") + fmt.Println("✅ Changelog has been automatically fixed") + } else { + fmt.Println("VALIDATION_RESULT=cannot_fix") + fmt.Println("❌ Cannot automatically fix changelog issues") + os.Exit(1) + } +} + +type Issue struct { + Line int + Content string + Section string + Category string +} + +func wasAddedInThisPR(line string, addedLines []string) bool { + trimmedLine := strings.TrimSpace(line) + for _, addedLine := range addedLines { + trimmedAdded := strings.TrimSpace(addedLine) + if trimmedAdded == trimmedLine { + return true + } + if strings.Contains(trimmedAdded, trimmedLine) && len(trimmedAdded) > 0 { + return true + } + } + return false +} + +func getCurrentCategory(lines []string, lineNum int) string { + for i := lineNum - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if strings.HasPrefix(line, "### ") { + return strings.TrimSpace(line[4:]) + } + if strings.HasPrefix(line, "## ") && + !strings.Contains(line, "[Unreleased]") && + !strings.Contains(line, "v3.0.0-alpha") { + return strings.TrimSpace(line[3:]) + } + if strings.HasPrefix(line, "## ") && + (strings.Contains(line, "[Unreleased]") || strings.Contains(line, "v3.0.0-alpha")) { + break + } + } + return "Added" +} + +func attemptFix(content string, issues []Issue, outputPath string) (bool, error) { + lines := strings.Split(content, "\n") + + // Find unreleased section + unreleasedStart := -1 + unreleasedEnd := -1 + + for i, line := range lines { + if strings.Contains(line, "[Unreleased]") { + unreleasedStart = i + for j := i + 1; j < len(lines); j++ { + if strings.HasPrefix(lines[j], "## ") && !strings.Contains(lines[j], "[Unreleased]") { + unreleasedEnd = j + break + } + } + break + } + } + + if unreleasedStart == -1 { + return false, fmt.Errorf("Could not find [Unreleased] section") + } + + // Group issues by category + issuesByCategory := make(map[string][]Issue) + for _, issue := range issues { + issuesByCategory[issue.Category] = append(issuesByCategory[issue.Category], issue) + } + + // Remove issues from original locations (in reverse order) + var linesToRemove []int + for _, issue := range issues { + linesToRemove = append(linesToRemove, issue.Line) + } + + // Sort in reverse order + for i := 0; i < len(linesToRemove); i++ { + for j := i + 1; j < len(linesToRemove); j++ { + if linesToRemove[i] < linesToRemove[j] { + linesToRemove[i], linesToRemove[j] = linesToRemove[j], linesToRemove[i] + } + } + } + + // Remove lines + for _, lineNum := range linesToRemove { + lines = append(lines[:lineNum], lines[lineNum+1:]...) + } + + // Add entries to unreleased section + for category, categoryIssues := range issuesByCategory { + categoryFound := false + insertPos := unreleasedStart + 1 + + for i := unreleasedStart + 1; i < unreleasedEnd && i < len(lines); i++ { + if strings.Contains(lines[i], "### "+category) || strings.Contains(lines[i], "## "+category) { + categoryFound = true + for j := i + 1; j < unreleasedEnd && j < len(lines); j++ { + if strings.HasPrefix(lines[j], "### ") || strings.HasPrefix(lines[j], "## ") { + insertPos = j + break + } + if j == len(lines)-1 || j == unreleasedEnd-1 { + insertPos = j + 1 + break + } + } + break + } + } + + if !categoryFound { + if unreleasedEnd > 0 { + insertPos = unreleasedEnd + } else { + insertPos = unreleasedStart + 1 + } + + newLines := []string{ + "", + "### " + category, + "", + } + lines = append(lines[:insertPos], append(newLines, lines[insertPos:]...)...) + insertPos += len(newLines) + unreleasedEnd += len(newLines) + } + + // Add entries to the category + for _, issue := range categoryIssues { + lines = append(lines[:insertPos], append([]string{issue.Content}, lines[insertPos:]...)...) + insertPos++ + unreleasedEnd++ + } + } + + // Write back to file + newContent := strings.Join(lines, "\n") + return true, writeFile(outputPath, newContent) +} + +func readFile(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + var content strings.Builder + scanner := bufio.NewScanner(file) + for scanner.Scan() { + content.WriteString(scanner.Text()) + content.WriteString("\n") + } + + return content.String(), scanner.Err() +} + +func writeFile(path, content string) error { + dir := filepath.Dir(path) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + return os.WriteFile(path, []byte(content), 0644) +} \ No newline at end of file diff --git a/wails-mewn.go b/wails-mewn.go deleted file mode 100644 index 742bb9722..000000000 --- a/wails-mewn.go +++ /dev/null @@ -1,8 +0,0 @@ -package wails -// Autogenerated by Mewn - Do not alter - -import "github.com/leaanthony/mewn" -func init() { - mewn.AddAsset(".","./wailsruntimeassets/default/wails.min.js","1f8b08000000000000ff94576d6fe3b811fe2b8abe986c5435b95e71880415b87d4353dc6d0f97edf5836114b434b6b947932e49276b28faefc59014453976817eb125729e99675e4991cd51b6962b4968ffc265a75eca17c68569d297d7d77ea8c3c29ab5bf83ec9a7ea84768d6ead3c1aa5f99ecd49ed0fe99e98c69cd4e8d8497ec9f5cda3f7ff723be937b5a6bb0472db3a0cd23cb2d588ffe8d892318e2d07479b71aa28d3533bc8d2682969f99dd953a2cfee1e1eeee87fb8787effef2fd0fdfdf3d3cdc0f48c4ef7e3acab6e61b32334bfb69b3497d184018483713e313236e7e6382778f1d48cb371c34916c0fb4b7fad4a3e39fc6c8e6c823bf75bba3ff561f6168996d7704a23b1b260c0c9305d6754fade6074bbe9aa2654260f01f3ff8081bb7d374aa3dee41dab2d5c02c7c14806f24f7db39adfd4369e19b6dbe9a3acaaf55772ad9e100b27bbfe3a2235e90d669e64b7806694d097b6e49c2208982fc0aad7dfff4445a633c3310b0bfc66b61ec49c082d628541ab03f5aabf9fa68812cece9008b62814cffd41ab3a098312f87a0a71d80a5fdd942d91af3057d6b8df1797302a963675450fab3eac011a6ae4876c0ba8931bebdbec6d72dd8c0debc3b7d61dbcf6c0f6481420b2cd11a9f66f69080d7bbe6b2e3726bde3103bf30bb6be64d54a7997ee765518c1c98dd3d81db0a316d8f5a83b44ec9b9d67aa3347125e1218fb2836f1997d95b3541a2497796296c8531bf795bda4186860c6bfd3318c3b6d0848ddb3ce32693ca662c7b466cf6953d335f52198f7acabc0e78371b3e6aad3499b48dedb19447210ad07a35209dc4f991ec8af697569b7e18d2505d9219828d64af407bc9b091f012d24142b03fbbd64e723a46af49044a7310dc9245b9a0eeb1059c782e7d4c089468ced0e5411d8817c1947c51c1ec1825f7bf36cd597d9c69b9a0a0591baccd31d86bb3bc77a905ad6f1af4360e1dd07a98419723d955931c0ea8c9f23da8a36deea6c2ed4e92ed791ba7fed634cb5569d0f712f510a6b7ae8d4c1c7d6e39895a81a822e8a643d088b3e14bb017694878096bb41fc94c6b433d62b717b0d1613b4a87f7001a88efd971ca99f911774eba639645d23ddf90918f8beeeb6b7c3dca0e365c423751be0b25887596fda2d59e1b980e620d4689672834e0640ded1f476fdda97e7a4babef36ff637e3b1d5a840e2f3b2e208e6db39c702b375b039fbfdecdd2fb37263b818d3dc690cc6288a4886fdcfc3d1322b32acb6f673c9ca62e53475b66bfc27f8e606cf6f8a1caf2dbe408a1c394f38b149b7e46a89abd85e854feaf0831abc2ff50e321ec9be22414eb305bcddf9ffef1b9345673b9e59b13c1355aefc31cebf1f4a972b49fc7b39677d544a808baaa3e71b63aaf892ab1380c75c2e1dcfe7e9c79f0cd82964c945c3eabdf8104793add0f5a258d1250829f9774189213786448b86cd59ecbed384c5d0082151790d15747e4c0b4813798c9a63fff85da961dac8f5b923f4a3fd8119d1d9831d061ee47f3985e28a3536fe0c1028a9d1b0d9d3824751df4945326eaf1f103e6f262c5b8b32b153b8f9c2b58dccc16692ddee60b777469d87263a1bbb9b9c9275202981e3b21555ece0a92d61d08b0905d6536bae4a8c46934d318ba6b2e395c9674851e455d31bbf9e56e6c3fa11f12f47c882949dcae2bd6511dede7886514593557775e5f97abfaea6e79389a5d8c5552aa5259acfc8984a38dd3f3aaaeff41afdc28fd91b5bb693e81bfa7d1d0fada40e7aa65e912e08d6123245b492ff8185e6880503cb30640e1cc80b4d803deaf32fb88dc32bce8670db6c348950e43a08617457122ee7e33b1a0034daefdeeb23d619d339706583c68bdcee9a4f5cbe49ebe196f4e6b3e4d32a45acdd35185c9753e95ae4c2f3a5cf85e687af4a1c29f42c94ac9a4080dc8ee2735f63e11f00ca218b5f5677c85da266c9d6c354354e1ffff211ca908b5fde08653347f462e77b32b8ff466d047b951d7915c6ed415e0bf989678b5bc8a7df10257e0feecbd0a76c57a05fa895926ae4337b89d406799156adbf42e1ed518b7029dac42288ac0ba9a3c2c1c976ae45c38fdd5c863987f69febbe9a76b77353dc641158fe2c2b75be5ff8ab917d5fc35a24d849b227eb356f1a988dfda557c1aae7f0ae76ea9c2b2842ea7f54009adff1b0000ffff4a20b7a14c110000") - mewn.AddAsset(".","./wailsruntimeassets/default/wails.css","1f8b08000000000000ff7cf8c712ef3e932486eefb29fe7735379a3d4defbe890989de7bcf1d7ff4de5bbdbce29c967a292e104015322b0bb56001e0bffff36ffffce3cd63f94fbeefff54db3cfed31cc7b2ff0b04eb763ffeb36e8fe6fcfd673e8f60f62b876cef3390cee9b2c848a8406114c62b84cc7002fb95485962349457d81f4a239bde7f8e269bfafdfff76fff0efedbbf81fffedfc4d53c1dfb7fd6f35c0fe55fe67d29f3762c27d09d7ff331ff1f7b3994f9d1ced37f56d9d80eeffffe2ffbbf5008fa0f0c82fe0387a0ff2021e8bf09effbfecfbfa4ebd96e5b39fc253de679d8c1bbfcfdf1fccfba9cca2d3be6ed9f7f07ffedfffc6baab2bcfce7fffab77ffef97f567f22fdeb9ffff15fb1fec7fffa7f1dfbf10ee5bffe99e66dcc86ffb6de655b37c7bffe4121e88f6ddff27f9ddbf0ff2fb223fb57b62c439b677f1200ff6bf35c55ff2b6fb26d2f8fff7d1ed5ffa4fed72fdb4b02fb8f02a225b76658e6cf277937c31a7fa74775338cc3fc7f7cace40eaef967b2fe5dde7fc63cf5e200150ac10d12866164e18f71f8c36325b1d484890bb376f067ebf067283686611c7c9a63fb79e4000e398661d4f70fc8ff03d25ed4b3560ca27dfd531886d9a83faebf7889abcdf56d12046e72866138f86f02f61f25446df16dcd76c5e4d40cc318d81f97f007cf72823f6b3792de2902310c03f57f8cfb9f5063e81f1c30a588f8fde1e38f3f50e60f88e1fee6c74e3f7918ff64f287cf177786515e2adc0426939a21891c8611f7bffa8c3ffaf8c444b09bfceb73182642ff1eda5f5295a10ef607cfbfd8c51886fdfd11c14cf31fea758a86c8479e2b41048611b13fc560df3ffa8afa77f388f58b44ec8fbe98aeff9b4f611485c98a3389e08161d80af85b893ff9d992e5bf1e10b3578e3a0c23c97f0f81fa833ac134d2232097d5e10f5f42fe15fda794ec5cd5b65cc0681aa923c3b0fddffa32c9df03ce453a86fe46fd5b5ae6e391f6fb5b7a5ea058e1fc6b346d4c31710c69501b09662e615906ab6cb0b4601ed0da905b964ce3086cc832eecc340c3357bd50a73a40dd95afed2a6630d6d3b70a14a0de2d79b62a7d554310b613a21797bd3285917a154e8a102bedecd5107e1fe7805a7e1949176e1002d8a196045d68ba297a377847696d09582c75ea743f7f52e20dd60b29db838d9308896dc78548e213c24aaf74b123f107c62670026b519d953dff939bdc65685a90b9ea0f629e55bfaf8c554f567f8b766291a6bb8e06311e8c7199d420a710ddd170289c125bb4ef16ed3264e6f21c169a7c4ded40c1ac43ab66aa8e8670fd241d73d23350cb4062aaeba1b6706ebd885dd07bd8381b20b42d24787d3f555598ac01d1d8b62d10ad4e90b6ad924a296af2612d13cf7d5103a4b2e2b26c0e1076ecfeedaa63040a1425e674f048008c4d2b7cd836d572a65b73e00703d40159d4e1f128be35711511a2bbed0f9a26c2a6d2305980c32bfa7ad0039536733bbf2dce366648f31ea112670807689dfda4a08eed6006e8b6fe390fd6e3fa8579883986c5668ef0c2b5b227094807ba78dfe884d8992e1fadd07604a99d6dce1a66b18acc5440d4cceb7aa53b5f203a24ea6ffa8523d194c65ab8a359d6835cb82e181a45bcc568f9a0b0b43d34ba5c2512b0a7c2d38ec4d536cad28948d78a5533dec75c238b50da577bfa58ec1b0b8f9d8b9108d79efb097349095b75a0c62c40df8bcd86190704ed9299935065d53adea186cd511060d5e281b27b0ad78c20a078d4a9fed3bb4b8a9393dc52fb70bda8bb69673898836e41a2449dc3597ea139d49d1a2ec9558009a7c606701fdf4fb53e271912069bd9ca64a5ec2aeefc15e1151b304e1ec3840b1336c424ed9622b8e934b48e40e64789dd2d1f9b2c150363663d73ac8c413a3c3c330a5c8be8b5b32dc0da19a75e55c6a6d76ae50786e4e6434d6e95c64371ba1925bebdcffbc1caaeafb1ee8b95659d1046f8b9de8ce4f806a5d3ea3bd22e7734484d371ad5df1192c23fd99a488f3a21c146ba263a87d846003bcb72b0e69ffece49a3ede4e5e7e171176dea2c058baebe48eab10b0d6e8d013c3b8d547cee44940e9fcd9de3e94ff72e7048a32d92fb4bfbaa45af4ed202297292e7b6587a4edb651f338b5a00f9e05d874f72650938652cfa694fd01d51b6171a19b68bec9cf089220855dcd6da0be62ac7c6919de299a1b52477fee6077a3566bcd031f2c754bf5b4e1d3e5a183daa25a740b8a7de21ce058c5c4d3de4dc187e606ca9673a8a6d63a6309b95e2506d6363b6cad65bc4427fcf136bff3946925946ec197364193bf5859a0c89151b05484f4327881a65bc17b48bd472e731eec93e166e26ba8b0239d94dc9175a18367230c0fb3d1a15db919b80acb805e12592cd0c43cc8d67f60023498cc112354fa047f1aa5dc223ac87a86d34cfb9490295706413ab4bc6e588d6f4457b12d60761b19ed91138014c07539ccdd3d4392bf73b303a836f2b7f4ee6edf16a7f14343457218c8548ec25e485af689675e947ee0534807a4d0fe0f8c1c9943500d301a454ec65712c083282ed62b8e533e8ba8e08c407f1632deb381883bb28a620b9093bf92b73a9944b56016d5898cf7c2148d617a91b401d1338931131286fba6330657024f24cf96b614e07b8ea2180ea800919c4a2ea42da71c2921ef865a5c297d51a3d1abc4c164c36d54d16a45b928b8dbc0732a268ac5c1901564c0bd088fcf95402496a6ac7191c6e95ac7757932399d8a623b89331726890dc51a2346ac99d533ad2f161b28388283860d6bddd879635c5c1670ce6724800ef7de5e497db344d662e0d5bf43ea1f2b0fbd8e1022ff6416f3ee52f609e189a13c13652e074ec654cea688914bf059e676c2489489247ec233e331c697263624af0014550ad4f4ae9632bbef4004f638418a87f1048bbfc9dca35039089c28e2f74ea7ea7e5d2534f233cadc3e0f9cb358b3cf162ca6ab3dabf0aeacb12cd9cab97bb5a422892472f19bb1b02d5edad963b4e2634bd421241bf9429a4f05b86319446c28e8cd8ab9435deb191c7f9e5146ecec81739665316660e693f7d31d9d17e56d1cb826b44fd5f808723b1fd58a8dbc77b36c15d26271cacc12a26cd83c6a0993a4f26a5c7c2e941aea329e241e2739d268a29e914f86da77a93278e65b957195945d75b421480499ec09f1efd6c32b994ccf6a2139195a44687c5cdbeeea3affdaae510e16da44f50be3c71e01d105a57978c13a4f9ba1fa3a7b1ed81afbd1f83f64eef5af8dd259963c39f4e5346fccaaaf77ace68800aa1aaf03e2f2c087183d2ea6fdbb2f52de65320e2c2becf4f4b2fafe4f20eff7a0938061334cd71df4f484088762fa3b60a5d90a10ab315acf36632a0aab8fb123c24fe0a8a8921c7b727993fbc6f53b5f97b4fc8c31a8d014ffceb5283116c6e447cddaf6cddd47fb905e7163b58c140ddf1a8493cd8cde9a71961453f065dff9d602bf86afc6ce971f7e9b2c10a3f4c867d6e000d814995c058d61b941ce4d483241ab68b8cb5c6f68f750a85fa1e204125a08a52be9c3ecf681fab781ef155ec13275f77f48404e594e4e34d23ed4e89fd5649ff3823ed1ac9783e9e499badf60afc65d56e0bfb075eb5e485530a177a34217aa2c67ae89312dcad73c2ed8cbc7ea5c3515278aee4de32b645a41a252fb271348e9b14a24637b3f0fae1de94524bd4c6f4c6011fbc46d87be9951f49b072f75689bd3403d0b78703f659e3f26a9c3af03406b0259dae82d368fa55511b164bf03bdf9687b3dd8727dfa6822092cfdc9f3d599e959321a573b68c66407e4cd32feba02ab4ae69a0d13c23367a56c9f54410697ad3ac527fb63bc31a74c62f0ca72fda0d127ae30bbdbc31c55aa91d61378b215be47ed0cc25ae2f5684ed67ac72dcfdcadd6a28b44fe20a2dd402afd17c4dc7e3862aaa82a92e9ecab3bdb44dab226726ec1f56ed96b349b5067fc63acdfdc29dcf2922cf499a24e98f4c1c219d56c8a047099736c643fbe28087cb7d1c8824cbf418ee1599240153b9a16fd32d30603b4b1c6482f94e0bbeef6620090d9b18f2a690bd804463d7029826d4dcfd6072a27577c679ef1b2fe477d3ea28545fd9f1457ff86a16836f5b24623d5299ac7c12f05ffcd98deae9c421db55cd3ea848cb393a455aa43301a2e9aa2e251491a29b759765e6e23886153105a75753c0e632dbd06ef7f98e3ccff34d3c4ba9599a8b64b9c9787ab21b49c6870fce5a4a8c601e19c1afb5c0cf217f6db6835ac3530e48f998974b39bccbbb419deb9a86b57f2a36ad4fbe041f7d5bc660be51cc869a7c18775982b06312f14112b1432249c5136dc66d30334f48a9fe69bde10bb7a13037cb3f3566cfd5adb1098f386b7d75277891a7c2446054d409580ba96c84c6edcc46679b6cde3ed1c69eccc028bc703379c4dcd99f7e3f41d83a913226617bc9ad13a764f4de48745c67eea9d69db2be35e7718adef0e5db389c079472235d9fdada53c8c544dd095cd6eb45cb0906d60fc4ea0ee6261e25735ef475a2a91d0401de77b42544d8e4666b21cc815ad1987af6183e8c2835a428253440e5b63b9000a909bc415007bf0bc0c0860476597db0bc9af8faae30b97cc8bdd2f9e74e665977a89d6c00804e36582abc426558c96c19a97c54c5c0c4a25933a64e24714890a2c62d7dc4cab8836926109ac4155d231654cf1b1629ecf1d20945350ac2d00f613e808e9f0b17364af1ce6b76026c7cf97de81ab2ffa2ef2059289fd2a7884d34b7fd97be6498a2408567668b73209dbf5ba39d09b54b4ebb159e3693e89938a677ff70ef49aef887ef4f36c32b5d9e08c90218911c08999e283974248ed3c0336a016dbde4109cdafee0d1429a103ffee6c46395cce0487dcba3390b4ffcf0a85ede2f7d23df1c3e8c1babcef847d30bf433d1fc91f94da32cbf9981fc92b4e544a3702623742170ecce431d3cacde0880ca1a29f326e5da0d44031618833135425bffbe853804fb63988c6173465298d4614746ca99cc615746da991fc37e8c2630e5cd7e8c9c3325c3e28c3cff59538c8c3135c3f58c1630b9c3568c183085c30aac2bfcbd5fc6a144fac3c10a2664f2b7c53e665d5bc51cecf8e83cd3d9b0ce24c009dca6fc29e24eb398a8e6b6a8c0ca10c7ba0c9a6dcf58cf99ab8c77d2885043391dd6089db9b59a0259dff9e514911ad0a5f5d98212d1650740def7865eb03b848001ae1c3f83a1097f2330c43814438ec667a48d0506404c6797cb1df86365ac80febdbd2d051059872bb480a40b32ab048077457d7b72f0be4f9b0ec1957dc35ad92f0f4e9e7c26f3dc771b98f5108b94dbd0bc643995d326520fdc720847367dd72317fabdbcf1c8f8ebbb4c801c07ab6e00ccc921850a8187f3fab209c8a8b62fd83afe9dc46a2186fe917f18b28bc5bed973a218eb970832629851e9dc255f53a3686aa86acc3befca48b25e02c74abecccc385e7c2ab8ba0c10d4deb33daad75d5db305d2b3c8f1ad31884fdaa0ad1ee62fa28afb6a52d910d1eb1974ebea94e698132152c8a41d192e70397788f1a6d9eccbbb255cd4c90fe282da8a9f0e050a4ef74f4470a8810c61be13234e5e0084fede175bc9709bac1652d11fb1acaba92c5f7594df46ca0c8e951c65b20925fd3ef002bc59266cee3d4da9b658c1244c7403e72d5e5beda4c48f7855da02f770feaf2739876141906278b4cbbe18922bd60fe98bcfb5e2f4d0f2d95f2ffb9dc7e808acf6eec2ee39432804c9aa882ee3f61c41b3bde6abf5cad53362d5aca37fe31db1ba5833aea373f34a27f1c502873f09546da1df847e2341002689822001921faa7e27361a40999700b9e0f06be1e4f2c93c9d9fb3e05b9c196fac7242dcb08bba09f0da2c11a9692589247b10165292a1da37f6e38b1e9e7245b06f50b5b751f4c114c6490b8e6ded368795c08aaf43f300b57ef570803f6e223c7d91d9f1ab379b53776c1b98f067860d4751a1b00751a037fe7a7ac733d16138f79f98ad2a3dce709b735b18a7011724fe04ec0b0e7bbac29fc1632bc82fbfb2c1c97cd4c3797fc13ed5ee62f10a4ff4b7a07d845879a984562232dd4ac31932e1404461c0f984495fbd672277ae5c13b6ba77b126b48da70bcf3009ce1e38aa320fb3cafa422e5b87abe80aa1151e16dbecef5815257005f0727c9e0e04a86e60223da47ac49b94f1abf5f69a6d8b5ac82c28c5e735d2eb68a6a0f11abd2d36befdd27a5f0ece11e6c29a0355d74c7434e91606c31a1bc1c3ba891732d289c24b7ccb3e3b55c27d93290a5fdcf2867357ddd673ab364e0cd2be35685cb74e4bf4f087b56d973279486a8bbae2dfcccdda80c079cd3471d1ccd4e04572a406be136b6d468ef5539d661f93a34fdd2722e8f483b89ce7793aed5499eb6855004040f443e1e3217b5d376004007848d51fefba920d951688614078b955f03eb8e55bebfeec4f381c4e475069e278cfc56a44e08670ea05cb899bc09a94af99a4c47ca135b09ec6f101085f26178bbd602b94aacbd4ed273f0ddfe2b0f68e834c5ba4a24f9b000554fa6ddd3cbae1a9cdc1ad8ea18ea95e098d410eaf12bd6a2b50c3b4fdd7da10b1a57340ebc8a1035352a05df503715a8e0b1620279be2c9acf709944e07c91a8849f67edbaf15e466610ee98250d73a9ef6cbbccd30418aef6599b761a7d99b6c6b650d7090bc14b84880069df034678a3047f05e71732011fb1dbbce6906e1125f1136e79383c77e8c9845b4b5a4d04bf453e06095624f4e17c40b73d8d9628119f8eb518edce00ee69126e9c6294cd37a8298b17a5cabab35718dcfa42b636dc7c06c42ccf37c695aa03ef4978e3b061b24321342cc31ef61f4c11fcbd6ecdd84a83247e2e6e92d008eaa07692fb471bffe6b8babd277a04a508eecfb10c33e25412bc24b3a0d4e0e0019c21825bb9326e96072dbf9fa8e07f9a8cd5ca9f118c11388fcb83d0250816b74bd4963420f0b7ea86b3b48f8b4d09f01ce4027dd14656855fb2d53273f2d0dfaf18fa2e8f40214ac750f108b8f658fbdc4033c2ae67e713b5ef6160550305f22b1314a280a743245fd235275545fa1b0b55004b4439cdda3d74d2798b4e1afb3c56c288b5b1d247ac7e790b4ebd813c910696284d248db5e2cd261fa3c51e7566939d583feb4733f715fadabec717049442974ff74793adf05c74e3e395d748dd6219d6c0c7e3d295d4dd606b8bc94d1afeb37fbeed3f7d422c41481fdaa6e02fb0fca918f98727bf203df2f0d4946e922b223aa2f4837bfd41b03a9dd4101e9d3d7a539e44edf1ee090db23ccf7cafe9ed487225bc737c883c21d4bf3223ff7f614607912b51134b60346604a978b022c1919dba590bab3e2881cc2374f06ea4972576c6c921731630b26acb8af8c39cdab0f689118cb4946e5881c2168426f0f7bb1633df10c9cc38d6ef73d49a44ecead4ad7295e57ee7527e9f7229913087bd22d5635200b437b2bb458fe08b1bfc78b37cd61ba40c1f7d1a852d2191720898d2a5d427a0d032351e7b83195d58354cb79cbebc3cd469b227652cd67cdf59e4e45b0111783c04ef7384c302e0ef4870bdf89f4f2714d5b57133afa010d5101d7ef325e73fba104757e3f29bc3e6deef6388ef5e354647e59a425c1815fe24e5fa60330165423aa1f54376414bef4a00c8232b15ee4f514e5a10f360faa6c3f51ba8da04877caa2802ea51d2565031ed772b4a685c0cc6fbc74556e8505d07e9a9231f760a7682aabbfc65d3cf6641b106a87cc127283290529d514247a2464c2311ecdf2842c732c8eda86be2443c12dae90452e1fb63d74b7df3af7728950b8f276183aa11e76516ef7e9922f2e0c38a6b2fbb3747ddbfc534aa90f4cf8af025eeb9d8ecfd961aaa6d60a01daebca6c0be2d10b2102e4546ed005c0ab06bb2f8f891f5adc4e40a5eeda543b8954b07e050ccd51fde5813f6ed81c6746c0e46b45bea84175948b15ba1abf40ea6f7b591c37f0ab5b0cd2b3f8d79e10c82eb6bb42dfb62b64246650f43bf989eb60f6a25b2c77df3975194a15b84055669e270f21d92ec11f23b23ff361364e3d06cca06b3cb03974a041930b2734e778c73cfe0985d0846d0836fc656cbf763e5899307735db840017786e0199c691d319ba3da757f40e70207561a9144a10c2432c73914f7b4557ea39ac2b96f906d061e5dd545142719015fd2301887efb65160eab42ceff5a783f8e26467367ca0ff755ec3d48827984a345e276b59e35bfd73991e7302f7003a91ed9c6d7b524b0d7319d0e4f916e24d1ab3147ee954e3fb959b0fc63e8e40d541f4b3706407df0a75f418389dd38c73ea14f5fa64e26a7a9c898e2bcfb232132d35f4f2d9f641fefc7329ca4ad67e853ee32ed1533e0e2cc1c0bd272b0687ce96e494799687b7ad4e119a4011407ece200a203b3482a2ec6db72a9089ddd0d1482c060444857302526894233d22effe5d304f241e79e7c85d615e2bfc5c9202567bb18f2ac6f8530a1fbc9c1a19821f7834453565d81262360d93a6c3cd218384ee4ca5438c3161d9f43116487a4ba258ca3eecbd41486bbd299bf47d79749e5b4ea2ebf91e3c3899eee3bb17101a1c22c634f54119a055d05383e4d8c730a4de7d45c9f93ee3a961e0465b4950b6b98a12295cfdca91e1aebc0d5bdc03c8e9fba06f4470ac2c2eb3d5d8432d27ab33b02bb0140ec6990da25144213703f43773f51772b0e1c6ce696f1dbe2b0ec0ca057f0e04b48847a3c8be108c0ed3b8526cdcab502c0d156b8939a8606d272ecb8ae0f5db369ac5d9c6716da296445c6708479ae5dea154dc61b5621d1631e678bb8893b7bb1d1a9eca7b454a92791cb81598805b5e01c51d244ddbddbe7d3a8c1ade5e8c9b1372c4ab4c4c0e1dbf00d904e807f8297439346722984b5b1df9c77e4ceeb083b758dd74c2a327ecda95ab08d9e6ed855a0a1de569f0d75a8b56a7092ea39a364c29dba11386106178e80ee73e9eed84ffdf1fdb9d5e1f90e17c6c6df089dfb65ff034f385824de52f5f0437d10c0e569821c8ca5240274c85b0495add439dd1501a114af36e318d0595626141fc65bcfc2fe8c215224e7259ad45f67087273b76390a02716b9736ec8541981b2f94108f453f59ffd7de422ecac7b0322e1667093c37b1ea82588758eb54c0d4639b82c944ed558347eadb50657c38f6466958b9a847df8ea454f58f76ffcf1fcc40d2efbf7016f6293da9919d1515c10086d6bbb8eb2b653d41579597e44a04d4a774d4ffd19eb13bd5190b5c00a0164158d687e02965635d52f0394726644b80b27b9cd6f9053b2f10bf045798ba004b15c51561c7e74322b37c2fe5247c8b8775823a1687772e29540ea7992e04961e2d688e08b35d5330faffa09dd1a7dadf48ac74b4ca4f5b1250e4fb09a8cab7977a4086eadcb4f5ca7171abb214c87ae0e0e9717f6c15436cbfa13390b2e8d711472a3af710d2e66adf4992c31c8093b2736ad46d2c790d66f90aef6d6d73c3d78f674a5260537e97b5249992a34107eed19de9c8cf4240980f866a25da6d0dd99b7dfd78480f6ef279649e3ff02c2b9e1085195c23f5047053574e7c4abc8a28e58f2768d83343a0cd65b8b247867aff10d25e2e4a2bb1adb4c4cbee5b7792c7fe7f86e47f42bea3c9e2afee28e93cc455010a170dcc88752c7abfb4090c127f036e7ce47283c3ceaa1f3a93788cd22c7449284af23f66654ac754b3dfb7aa03efc2c4f7c67ce876e36588e022884ca1df13008cfeed1e1a3789e572abdfb1626646837ae30ab0fec73188b838e28fc066e2a2171b5a0285df8f474b39b3dc6bc966f01d2b4e64e637164e0270a5aa97f88d43c118a5e091afa4c9f472198de3baa20dd4ff4e82bb2e5be47ce51b67f0fe7c6bd7f69bd25a464005ef8b99c7085d31950859470752310520002a2028296567a69200f72015bfc6cc6d67eaf5b0713b7ca0895240e23f3759a087c236b63758fd5a1ca254710658b81e139e3d4a57e3c54d356ca6109268ce9d1766f782321e5868f567397c44b0019bc615fbc3b0c8fb3309fe836c34b306b2fdbf36487553d578a5090492ffa513a2326735303af5da9dd5a98e92e0f5fbe2250062b9a177ebbe5e1fb38fb981e849c64f0c2b70e69e17a2f419040a5e4c502bf48a014d28c17760ce6f5b8be987d891890d09b0e7f46b77d638e6e130aab16299e340ca3f645f6d018916faee0fa1321ebb25234edc8616a6d511394e354dd18062a62b1a62e595e919d04d1afa96fea87e7680e30ddfd60bb06362cc5543710b7dfebb666f3635368cafb2f14c222532e4de0d0b175dd8774f8d94744cc5afb5fddfa0f637e86580aaa91f2afdbdf59924fda61c9b182e913014a51131b846a8ba25158a79ff86e8b1d922883c0a9159b959a8a36285f511ad327003965a39b14bcfb1a8f0d9e58b656e57a53a274c2b2df43facdd174e97108eddb46e0bd32d1340020559951f07e20097f25f3a29ff342612b95cd307ae706e7af6667cc380f38a2d8ad6625cd1afd644f24e64db518dff42555db236c9a7e927e265f93a331445f1c85caf4179abf87a96c3356117250ac94ad67316e73993745d118cac12bd25fc649582e8db5b48aec77d5f7ef5625d34db6be79072f6687d93582f17c385e60566b68d3048c75b77b0eb0eb8890c4dca11e8bd9a3193be17dbc5d97f064da3e42c75f47722cd285a04e029ec8bfe58323f574a071cfd0b7d9af2007ecaa2d35e467709b1a04b4cd0733120d12ce4105c4830fe69c4740e9a8d3455469a9e52577636c4fbb49abe43d16e3cf16cf1d3c894ff02dfec9c1b68362adf9d0e2f3d8dd5b4d2fe2fb288b63b0f10921a64448a34cb8d79797cff0c00a1b2fbb88999cfd6d17c756a9910d2da737e61457efb573a31ad5af3335754c41b0886874ca4a6804c730b6c9cf69d6cd479f45da2cedbda9206276f134d690c2e29b6d8b68f565cecfd5124e8222c0d12e3af829cca6e891ea9d1222485f248d7e9284c26d32d12551843830c7dd47efd665fbf279c6234a2ef0435217dd4c597a02eb4994250863663870d59835902943fd768ada2991a5cb517b9206b5b1ba504fd925e7b56d2dce53e81d2533d6caa53bdada9ae497babe39c96ded9dc899e149fc5de1484fd074d23adeabcb6969672f97da0a1d0a590499e99c6a1cea55cff03ba87b09113d1ec4c5210fd24cafa12473bdb5809ad918abe2a086dc599c3043ba44dda455df6caad8200cffbed435819bb73844df08f90df1e88ac9373ecf01e6ca5f7797c8ef250b064c5da997480a355cfb4d5430772b6537726f8944db6f7a02f9e2137043d4c2fffc2438e0231d4009ae6a7cb7785cd08c3f11b7a7577f23dc5d5521506efc731063e0399b0fd9cdb0524fa1f6219b96681ac921ff008371c2c59e3a7d9444faeec8c078d2e90fd7b8b89ae04f428b262ca501f26c5fbcbe4faf14268a776fe22dbfd88b9f5be11bd5ab55bf4028ff80765cf2e2b16e88b486b47ab6920e09a864c8ad26a89b28d557a3b55ccb3686cc884fda31fce51adc2bbcb58a32bf686602e5c79e60a2dab9bd31f84e54df995f25d7100066dbd450ac13d46967b4dffc3efb8a1bc3cf84634ffdad3b3a3039f64c3e2cf98eac7a76ea9848e78bce0384f5a2fb2e9e239ad3639656f7866bc08a33a4212e3a59e64c2166bb5bbfa9f0af03b269f61cc9e3cccae8f78075dbfab986e881ea5a3490b08addf16a126c634340ed9ae5b96c7af8f0a3d210a0bc6cf04c8b0e482f6b64ce5efc392b9e1ee0be84f1e107034e15976ea1b617e31b88616523870782e7a97757c6a8a93f2e1631e1ac181949f919ed9973523accba8dd26d9bcbc59cece7097a20ec5f88398e6a43fb8d013f23d16a9699b958faea8895eef7e22d5670aab95617074d38515b837723ac7ef20c54641dba3a88f8a68eb3e00fed851449a85a2b263de324f98e17066d1aac7df3c117f4f3eb025b2d858835bc107b8bca98f2adb2c4fae0971c80bfa6ab8c74d3fdc5d979e756655f6438684c86911be5bebb7beb137e6cf38a0b497443e836256fe31b6eabe2ca8d7a2b144647b9b6b5336b5a577f2dd647bdda96f161d96f9dcc7698cad0979e908e836ed57ae70f1a5ffa3062641c5424cb68b0d0bd810c7c042afda377f35cc502e19d7521494f51494aabf64789715a4ab18548a0bc751d64dee0921eaf495f840783ecb444da9f0b6ed066e9bbcf260f11ff05a0215fdd81f5aa3069a3d5c933619285b82b1375b5545adef897026aa333d0e57cb4938fa6dcd5d5282edf0fc4a64260019ded1242a7919f7d320f8673feeb31349e5accf11446b6b3f99cc05ca11b8ad0f5c0663abbd188850c3f745f958c3b77fbebf22760dc3f4e3e3ba20c19a6119f4494422cdc61b65cbe95a56913de694d9645c398eb2265c7d08ea231e5d9fb5200bba7aed2552c095419fc4a8bed99229032eda98fd373a28d2f4c2f99528bcd3e1f300687cefce8c3664e3ffac1e48f5a3b64ac7e93f0ede7c67444db772261b5f80815e7820ae662d97661230431e8fcab3952dad40247447da01fc42983b822cd5520174c40070ac9e23f137aeb5934f446bb037de9bf05c89c45f0268abbec383740814438c8d5a8325660ba3ae8a248cc2906a82b6503bfa255ca3fcb4d7096c72610166c239fb22bb595f6f05c418c5f120c24f4b620b5af79a345eff8a416923472818e51532249fb6b64525b61d7ab2142ebc133b071401ca862f44232542f160ac4cd799524d906bbfd6baf2a2816584bd82e779d2a579d60607e311b1b781918acabbcf52b9eaca6df299dca2628a3b6dbd05ceb7a5f0e70b286cc40c280a5f71124d87cacdc5aa26ad79e3c16769d4a13e2dd625bc71caea54d3d748d5c5d1365542cac1f1b0d554dc38570582df4a726ce14d5e92220d1a39943cbb9bcc0225a2bee8930dd731bb1e30928de50df2afedb5eb556f58df12959d600b27ad6f8cbe87ed169db3ba00c640d7f40e345adb7a8fa0a1cd8048f1670288fe1930c516028af409b2642b0547b8ed872588e75e79c5a79b9291bb3339d77c171539d8913d88189847170de69499cea625e22f733ab17f543db14a84ee84791f26ded50587f84664eb75ffedab288f0a74e06f1215714ce009861fccb34082970743af27f59177a74fa22c760e2b5267785f6369fe539a692d7bda918c347be0275269923e6eaeb1707edf88707e4c78fc43f9ca8ba0d04dcd88edba061c9629dd09b5ac8f8d04bb407a5be737f21da4aa22a098591aa11325d19f9be6003baa91f19a2bf7139d1d79f0ad55f7eeb458dfcf2fac522ac0f0a6970f66259ca81615713d50a0392ce300d5aad1f2a6d1457e18c091da6dc0474e29f17a5f115efb757426eaabf1d5d5520ff11a20cd67b9257eba9b8dd30d4817df399954831d95f229ad45fa876a671581d7f29afc332fb009b0f81848ffe75b4e4d7d23e9dbba61fa923da3a3668c69d13349ad03f480c1533c0dc7d5078aee3de7376f3277172a0e81a9d9e3e5f6daf5e24417afe1563e70e3f13acceeb90519c00e81a0492f8d76bfaeff815255ab5083d6d1589ce3e760c56b4d1ec5bac9aaabd768fc7135dab260facc83b7005f1be06013bf679c1eaa03d51c82acb8f036ab306db62e501216f8d5079b3a4626ee1575b329b64de96b421bee983e7ae53dcad3dfd28a88c792a7955075e5091001908408dd8932e11e9b0a9f9235298768f201ff0cadf34080b5d16b8a9d9ea2270ecde79813b4a000887a2ac9b9c270c40c208cfaf3396f0f42cd8ee60f1cc4fa89f1188f4dcead180e82d848b1dc73fc1d0017e810676cc3e04514733f3b36ec4f7dd944c5bb240bc1ea22c65db9e3c7d29d4d6905e7e6967c6924453091ac47ff965dbbf2881d709aa721f2726f25a7a14c7a0a96830b54762d91d67b0da06eb2439e127f67135f1d6265f23fd6ce463e0a8bac11990f325fbcee3b63534b1fc1e9fbc69cce96a8421233cc6d705fdd5f7fccb2425491d98820609c21aff2e86fa55e6dad7acf1ad872567678483d16692dee3549dcfdd96c2ed3eb5f958c2294333dd5977396bf0f07f5fabbe1d5c34ed36f61666ad98b64987dd49aa687ad20fd885b1de6ac11412d3f30cecd413312e18fa3d280c70820c34b07c5f65728e0098cc870ebbda140481e322590358f721a76819a2400f4fd84c2129f6f3b32edef9b9fe78616857f1916a3ab642633759a39d3a053a492bea476334be0149410742cc787ab27bdf9364b5281566af89fe916d557f1757ac6acb17e2afa97b8d1a981d850c2a34e0546003dbaf909a06fd760ce911daea3c85f3bc41eeddaa74c37b0f803aa5a91f95e36807b5813d1fc8de5eb2389df2a9a379294f542cfa1d3b669133efcad7289583a839c5833ed0b40077736740ed0699f49219171ddb572d84dc9cc6cc7c1a331c4e6f86f068bc19ea306eb69713c85197c6bf34f331695cde24486fad425d74648910254f09a452326483a2f3a84b952ae585a73c04c76890d6a38447549ad85074da28990815a990098d3815aa0acb9fd379b7d78f852431d928148552e898accd3bb080956f6cca066f92b469d6269e5d9aaa20ba82308c90bd0f9a97eb82a30b953c143c803f398fcd19e2b449f236063f71109bec9d9c23f48c85684f57997f1df7e61baaefcc2609e923d65fea8738c3e2bf31864292adb20d0b2f3ef6e705bccfe0aef8d9277d422676b18bbf3c4b6cafbf673ccc967575ddf860b9b34b579851a948acef332f35e65dcae0876d52d6ed561d51f99b8067759c07592d1bac559db3d7361c3aabf1eb175132b1d675bbfc0e1014ca87ec1114287c43a128df5c48e05df799119465f5523ed89e4cb6344bac2a8b04164ef287ef4aeb24db90911d01b84b124dc08fa85e8748d5976e7d4aaff39bd694bb30044a7492597464aa7221097b072c367c461edf1b7a18b08ee1abbbc5ae13c206d5f8ee2e3e78f21fb51c07d27f1a40b2a769703b12f3c13e43eadc5431729d515a91f3a1e7893ee7d8c52c916d03c418c5a888b26277c66e003754227156195d6f341d8d743d07514e5c4e1109c5d08711763ac2246cc1a77a80882b6cc4935c5134d18486f693329362dd1958f2f54ee35663290cccecb38a248ad68fc88f632ad7d9a2c396187a3edc69c97dcd11272236de739cd3577e6bab7255378928e319dd16b6afacb4feaa1489119dd63ba406efa4e102bf4769733aaebaebe1f2be42f0e08aeb5e0e6a4b69800fc3f967de8615dfed3825e8fa9a38bafa3ac43b8c82b1159dce489e0f998c7c4c435e5d1fb1c6aa07de88b1dcc905d2371cc2df01571475f86571fb153357015dc68f0413fb299bf9135be242ac569f3d26f09e931ef6776f7e0f6a1287b4550fb1fb0e25ed32290e704e45d0ec830f858fe6fe90fd5ef3af3d0f3a066e8bd9e3ed6722916053c19c6e632df6254fd9e7c541896bd9469767e9b43f5ff28ec2a07491eb2c99f8345edaa6b79620d8d9b32dfe9c4591e81781141650ea0e0e535481ae59ca7e185d4164f1dd50119c6c9d1c67a847f65159d66aff80ed3a54ba39a6b5a743ffc16d18ddefcd0c7ebb98a428c7d03ea008f2247bb632f9f527b00d5fb9110b5b8b3a2f50e212919e4fb22f92c2687072779a5ceeedde106a0c64730e00a759a3bb3dfb7d94f3d49228b50dcc2f94624cd7cf6841155ed6719a400d0ec3e21ac92eb5862ca14c80a077f23e0a6aaf3c9aa6f38b908cb7a21f907e3ec2ff1d35d4eade683eb79cabef685e41d371b49e485311279f833033ea4bda67114d83f36e5a6bbeedd83c2c25b4a865e51b608048ec16cb390e98a16e7d63ef680fc6085596ae8b1f505a93e59976fcd534682097ec2c64955397c89f60134437b454aa06d2ee83ea70b3a4180882bcb65855dfdd7cef60f26d13d6ce6efea07c1f99f0fcfc4bd97653de3e1a103f84c82dfd74c4cb5c4705050c7afb9b5915a0ce931997dc4b429fc8df1475bde7db8bc60793b2fc46d41b305feb6c881f3ae998e75d1abb7f4452802d392eab1f61d94e14ee46552369e3b4ff16d440860b82df522b461876401bb955f3fb81381817d2042ae1703d525ce4c6961926c446dac67655f256504a4b12a7c683aaa491bf3601de0a02523f38901742c670c5dd7850526cf3cc5113e3b444b6a051e3975633fa28f709333f2e8152266db4cbad51a60e3ceaa87f441bead01039ce46f1c30fa614904de0b1e4181b39628932e2079cf871d9b02bf24f034c089c55f256e92b633a973457990ffcc86094805780970900464497db7241bb0def71b1b22e75e27db6a0ac170f2e023d4b4dd6623fc2b9839e261dadd396bc5b7be2fcb31b6debcf12f717790a012935eac26fc5be7912bcb096a61fa67841a974af74bc03618cc31ba2adec6a860d83e8c2659e18d32548b75e03beccc1b2ac50a128aa33908b53ba75cd5f50d0ca3bdc04ba07a4151ac825c1a42f46a5c8333f363ddef9e4272a5a6f2533e6b4fd5a8599d558d1e8af9ad6abfeba98961f2c43bbf0442e22cac125ce380626f5eda70c52d35e293eaf6e6feecb4188ed52b7214358b672d5763401e7766b65cdd60b8c96f088b58ffa2291a1013ed0d0028273f0594832d14addb0716b0e2405c88fb961e57e4acc72d36f744a5b11f98291231e994f48cbc6c6500a8e3aedab0c104ba684df0a880f19958be09512d60ed407e36a13634a2a2bf5e8d044ed629924b26f6a487d8d48c40f5ce92040f9dd5b5ab238454b60b5f5e8ad5ce9059951bdcb755ec55a1bffce3ab70ad9b72c4b40835a4b2cab4174ab01474efeb74980c65278059ecc381302e9ace5c51509aae669a74a5c8c25d94850b2c2285d976e4c73d136d5239cca95c01ba96cef5061bf9f7db3253e75f4db56a625bbbe63e2bba487f4454c6da4888cb0d17c9464a38ab11ad6474b5563808e22344c0cd2a1d4b3c466c8fc449814cdf13f3839ea18f9d5db6a1168e933637d9c1ce411fd521bb295580f2607481376ea31d85e19d607336d8559e77ab4167d80a4dce89065c26a9bd9af97484c37231d03f4d0d6336088c3db567ad396e81fa2daf027556397ef957cc7467d7ef54512ece219d90baea178521703a233be42a55b4bd822285de85672a89f00080651fb059c16100a5748ee31c315936ee3da55dc89bbd50d385f05268d904464ca5415e66a425632e7fc4d55017243790f1cadb9731e2ba656037786eafd6208e692f48b9d3ef487d428e1998ca86e207b8793232f0b51f58466c322d8fdc30bca11d5c98e55bba4b70b8ea86344b70c3311542707c9a22d2466cfb3fa5967e199385eba374f57d674217c0ee4c79b55158ea6323d3b7d56de4b669ebef2b16e3f884c7087bb96c0f271c80065948b32eaa98902b7ce02c69eae0211269b98a136c2ebe5b9253231f6bf36e68179948814a069ab8f3f14436694d701a5406cf0773702721c9cecb9c859c13ce97dfb51d7d1ab543bb31df747e25f25574f4f3a4bd53898e78b22beea54b844dec50794b41e879f79d01b0704287495fa3bf07c3f7e3375a4c2a95366fd7fd374d73adb724b00860f8802b712797077e87077e7e877de2ffb6f57b2eab9a7985cd1a82b60a549fefd7aea985220f8453b1a838045d8f072499665c73db59330fe69186885b5cf005465b06827409f4e113ca86c8c1067a55510b4bcde32d70f9b1ea4524ca17e83f787fb86677d47011a31296de12144fd74cca5ee7edaaa5095941b4f9ee79abef91df509ab982ce3d9a90939ec75005387358b4c45a607a0da44c6b8125dfe310f1d7d53f9fb856439bffad12a2a56a3a567ceca6ae98e7cb5b8d003ead8523413bb5146638b979752b0d4f8d243d2dca9f3970d3b5efccd9f78322bffbbd06a789fb248f5c03394074368eb8640fa83944ee901ac9a59ecdbd0a950ae3b27d612010ae32f8b6b83883ab003b613b16e042b5d92ba056aa0ed909c5b509b66558dd95bfd5a04d6e28f35fb58d078c948b7ae6d62ffcc51a4abfbfdf73bced874c5cf6d4502126b014f22795af520429f272d41fd251ce6a21625feddf60166f42e36752c819a2e4d417e61bfdbb26fc83a634416bf19fdd8b53bc2818b1573e4c6df0f2566c00dd28011da64b617299c479541b8c6fee532d45fd58f85939d1e0428d45fef93161cefe6360aa0af7b40144b644fc76b5a0090f5ba5eaa9f3cfa85a92cca75f4b3f57461649d8ddf1d4a7a27930da6eaf090cee584e0b71adc700d528622484ff32fb0d6acc94b5305b3ac50b59ca5a402d384405795a0e94971ac696df9e9a2e59e5d7af7b09f84fd693a435210e0f89c92302d93e5f1e7fbbe7213658225dae95299eebc50843246d5d5352eb86dc130d78b330fc2242da0495a3a3c1008a2449f9cf79e3c8f52a36257f6cc8671ed9aa2705a3ff54e3ed40657a459c61922199b90d1a821cf0c7a4c9db8e1752a85c7951a6cb8579a02a79a0ae71514dde764c5ca64fc05eb0551bfbe7bef16e59b1f066f1095c8a88d57901d239ae07d3e815cf4cc6cb9c1cce0f608958662ed71bcfc775cd3216bfb50ac052ce107d05da948df65f57927b45293073d402be14a13723b28c3d3ac70a7bf584f91b9ed4bbbbfb7165c4f36beed825762fe79f560f31f0bad585f3e22de4c2e61dfe7c84bedb873e8024d3415d5636dfc82b82cc810dcf8c3d80d23860b2f12312e62091ea75d657f1ac881ea1bcc20ec8550ea2a24a1ee74629a80e682193ced79036b44dd3b1038af6ec246f2b3292f83c98d9fc4c991c01c8b158f0ced628e0c228caaf6ace2f4bc34785b18abebf02a9256367d2eec7270c98b32b0c0f66b3b856ba382136f1b120351d1bf56373a8d4b8b3643c902f1a2ec042f002cb50b89f672fe2cb147fa0cc700a95cc823caa0cb034ee2b2ac8d0c2c60803c4b426f1f517a3aedd28d7278ecf3f1c880fc90f8ec49437f4cd7de02a5778740191f0e1eec446fecf19c5d2986bfb9b979be906cc517607e741a61cb0141af07c146e9908ab31a5fc198635bdf8ffc6d68f398a13de9b7b159e00d7525e34a31f4f5d3f2e4f025cc9b8e28e545e8f71400d8992448929b50209b45bb26164125597c242d9957748ae0368e3900538409ef38555a2cfa8bb82f51236c33191111bf7ecf3b255b776354cfc908ce5b67ce13b55dc150dbb3dff35c4bd30f4967c7f7fc87b54d04f2f4544f3c8550ddd7683a7691086d9885ba2035fc2268b574efa43056770781ce310100cb4f0200f71f7be0bf4842dbf32583c2062dd142b99954444e4ccc991c88667383c3f6cb26a23981bb77e89cadc00f921dc21692c17a97090146d90886c6adcd64420a72ed7ba1188778642f96808b91817ad58a7d146bdfaac548e6ca09303e343e379c441af08efd1fe6ec0bff7afde43ed296ba52a015be94e3affc8eafbea37aa6f35ce11c9466acf3b3b8b6dd8dbe5de3b17de75bee987c3ac77c0b9bcff996025739be8904958527dc7c14534eeaefd7dbd6a5c684c0013654c6621d3a8aeb2216e2b47b99164e2d1b350061a5055abdfe445bf445bfb8798393823d2f04cb0895eaa82042e0999ef2237144c2dbfa21a790c707c0bd908d049ffef722204236a6f5532959a05003d94d0836189a84585d176939ddf171d93dcf88b9c74f0cae848974dfa3d954e2f2ec80c314d68cae915f052f378d3995940ce10bf1f4a3dc3f2dd9a3f803945e8dc835d67666971d8d6d92145ce0173e15018e976473d5661247c6583f2b69b928980bf585a6af192e74f46e7037e109c758f823bfd2038fad4b362c8b60c433daaf2f2a61acf661ee2f6bedde2a30013a8291b3ea8a14403fe3a6c56ec31432ea47af5ec15f742dc2295494228cbf2ddbb613262fd76e25fd73fb83a13e5badeff96e7b580ae20df5898d033e86ecc378aead6569362fe5a211a25c3f964cec72d491009e1ad29639708f93e3aa30ddd21f8ab3bad1971c0c415e597fbc312264df7df17d0dc37428bb874f12a73c0bcd36cbcbf315a1da0337b4860fbd6f167a14de0d5a712037054befa39a165a75d87d4c59763c109d9e9ef701c475eec5f2543995879c93d005a6e1316c8e4ee4d0205d759df121487c861e98d004295f688100609a9f01e890e5a303008057838a1240094ed68b67346f1e056b9e347b3a89de95069c47527cc9ce897cdda674c51dc53b66b8f20b4120a3588ae2a91e173d0df24ec22f700fd7692d0ede170a3e8dc3aafb4cd505968fa34bd3a53535a6238f5ae0c409b7ee4228875a3d654acaa285956a10eaacd7cd35939581c6908cd8a198efbd9e002f61fcdc184cc7f4d90cf4a2a98d0a80a49e5a0f4663af867d11422ad0148fb1bed3dc7fe779ce0c729a2e12b3e0889cabdad95513fec6b5bbd58b96e09f761981ea3dc1d1f0b8f30e8079d22f3f2598368cc0c036bc4ad7026bf3e451d9eec4b085fff25a2c0941b9f75ba92715c5633cf6237f335b1d5f60a717c49dccc017008fa41f620b96a97cfbb9c7b5b2b7f9b7bd5d4c78ca2a8f25d7666f2413185315c0b8b94508088f3eaa82eea2cf47e09b26b3cc0b6473708103fef247b2997d6d8b4f9fe8c153e4e73720a0639c50a24be8dec5ba0cd25894bcbb6955daacc350895e9f9a83d0f6d0147598194fb934127debf5bce6c5e32b549aa367614896db2388a5d3d083bb7e4d9bbc0a7ed71c60c711774c539db6d0740e564ee183bb491e106188dbd09a92568570e8190db04a282ea8527fb6228d1045b6d01896fd32523a920511f30dafb4001ac5bbf29e38f05db25f146e3d8670cb5a5ed394eed8a3efb193cb08e3cb5775ece29de4537b8e58849f4340656cb7c82a99b43a24b65bc5e11e356aefb94630fc4201a9b68d628a0306b1e89a79f888c56bb2139ae84aaa5475da35c7b6081da86738a139837b2c597ff81e5b6f72d112d1450109ac3043983b56c456864a3a5750316205915d02daf7b84632278a6157632ffd515a89b465b515d27df5503ddd865a34ef562500ecf769945b421a370e2b4dbd8cb1a9a3e935b48191613bb15c1bbc773afd887dc81da827ada3ac6450765530420ee20ad8b4a977d0382da8217a3bec714a948fab92e310bdaee644faec294c6832bf064d976fafe08f4ceded23dd742f0e14ba8b5da4f2d0925032294ab058c0dde8129377667a3563ac84a392ca1510b1d463ce7a8f518238504234ace82b9518b0a468aae323e4a571201d102a334ba9de88bb8a5fa17a2a5a21ae27cb3319872da56376501661eeb5856d645b95c90c7480fc37e5d4cdf3cafab01b6fd08d6f6b6ac45d158883e6e38c7b4c1a272dcf4d649943440e830d9a4e62dcf9d87ca0de0396cc61c1dd3fceef012ec70b9e44be4ce740b30106bc0c606784acf6c833d313fc29ba6114105e5912aebd85bb9b241604fd1ba3d528de1e721af2931fe2ba1453677b71d8e485217dc0e11fccf56ca6770a2fe618b36625a68f91acce32300caeae87c7a45db5e2a96b51c62adfa02c41ba7cae180075b8e56fac3501c2b589c5a834b32da15564ab9ac0aeab18a61c0a731883041a29a2208d3050ca8f1350ca25a69ba100175d80f82477b14753dc7580d036d5c32391d3403b5fd39aaf1c14bd97fdcc883e2e36411e9683f9984f446e8b39f79341a8fd81c63a958db96b6211aba6c12bbf3a061fdb67a674af025fb51f313f9a89cd05d9da2e752ea50751c8b993f69679a0dbb4a58f47821751c4b4852bfce3a316d6d218a0b02d8094f7119b2f871b748e1c48e527f880660479a7e4704e611796a8978133d13f054ad221e051aa37a9eacd126dc32cf2d0d09437eed3a4a4e6c4b7deda960000bd272ad6c9a41aec5ec1a4d08180f4d1b1cf9292c1c1024140dd0b2d14368978a6d713239b6edd6c345042bf6123575995f5212cb2618920c78b1c6b5030974acb8a2ab856c8d46b541cbce46c3be5c5c2de00a8e771dd76d5d4e0dd265633a29dc2194d3a713e2dc335826f5035697a9904e52f665163624e8a7b133fb6e1a5b9b9b35b0e53dfb81b0e7f83b709a5cf7cbc128c36b6bd4a31dcd86671a2fc6885ef099ae8df4e80ca3bbd252b9d08818a9596ca2237a0c7d9094384af7dc7e6059b33c325901703581d55714d4c1db41eff91ad4579956dc653ee5552d9ff7438fb851e605447d4a5d88b94f446cf54626747d4b43125c18961fcf23547524245fcadd52b94ea91b48225758215eb6cd99cbf1bf33236208adcbb62d06d28964753abb62a12888082fc1718d8be7f3335845192f6b32f548a4a9bede37450a3de521d7b8194feee9f6b88e8fe262e93f40b93bff2d10a6eae5f6fa02dc90c7f8e35de2c2c6f0b99114b62a6d66ee82c320af77b0c6035dea9a6614f668655df1ac340d29fdfa26e9c0b5164d5e6098d3b04241195cf0cd9c117df04160cc768e0bf398f6a234469b4190446d1e0da384f6ccd686976c6e6c3f45875315666b1f090354aaedc917d33d004795aaa087538ae8b2a1f353f2a9b402ce7bdfbdf33b8bbb22f4ab0751db41e4bbc3826c218d9c2185eb0c3520479a8d86d13b00c57799e9b586d5589a820daf3bf2c79f118ad4e754bf610dd046334d22102e85325c061033e7f092b8befad31909fddfada014e562707e26f7407c03d88fab0b54435cd747bc2daffda9fe98239fda054e397db73cc837859c7cdf70f38e70e4a7285d0f14b1c6ec26f955962ce9a4a63fc3dd325089a211fc39cca3b4494f9e6a59b2040b3f30d6d073f2ba8b2bcf432ca8f68291cbdc03052436903f951e63663f9c8af86fc032bc42ba66d20bc7281779ea60e83183d19a0b9c2e99720bc7e8970167e08ad770a45bf5f85707a0e60b7f6286b86a0bf627765f14780d12fbfa20876f512e43e31972712fd45df80f22391d20fb084c06f731daa16a76f94e0f9968f731b45ecead64c399ca26e02dd058910f4f0796118ebb00e0fef85e2739e26f8da160a4f827f12405f2a7b55a3b14edd90797efc492ab87926ae24feed1d9e862fb61e1f6d4c9fe19a1e17e9ee362e2c601b9dec5d1a36dc4d947329082c07849170db44641a82d1610d9dbf905795ee00705c3a8f4d6abddbb8b66fe22dcaa1eaeec27791ebce1dd58eb2828631bd5d47e6d6f9c92631dab22a4bbf31265dda8b08e419ed7210fb57642e80f60382768622d8ee531f3b123f88cd89166c45eef0a84a51f2302db22cb238df431384ac30a34cd9cdd918d4ad08cedf638f066c373d618d548662a2bfe6610f765c842373753d2980f47bd46723c2faf0ee5e144fb6125379cbbc90b144867c8d0ed806ac8dd9fd5abe22dd1f1ca61e4e89763a428d096b67a2388bb6f6f40fe7a0697ca3763774f3e8dc626d67cb6d925f4039bdd737fa325d65e7f4e68c45496c4f439ecc9ac98efc91af04e88680bed0fb246f5a6cba163dbf614f49fd344f1f8e818ed253b5d5311c52a879043552b47f8d4f282988e91280eb829ddf185f97947136189075c1a6a892b9ac0953f862dd221eb0a409172df5e20891fc93953c19c4ddfe5956f84eb01b20bdc23706b752d0f731764510a7ba2c98cc0a9e90dd0137225afb3dae3b44fa19d2fd20428861c63f079710ff4d0733e37f959137d724756e405695a7cc10a4968b9831918dd94040a904f1538102c077b91918dc68a5175644cfb21ac74666cf0e6051ad7121d7135745d3e8de26e4eeda9599a0a8be5b27a6cc13f3cfee8ede5950444a0ed57c5f99ac01e8843d52f354aab41df6e66a32f895536779318d93cfcc1e649741acf1a865434dd2f0be945fc8a525aa71f5058267fa186f4c2131b4c19fcc6af13ed0c9f06aa06d1ad2e155ccfc81514436dc298017b35d5667b0f1d425d9af9d254e745af363c40a6e0c9b4c12cccf8a60b98c4831e27e61bb2388265113ce86b8fc17a75a6d22a54763f090022081824e98d3b9d81979e78a12ab9ce3ab79c2caac02e6ac2b0aa4d55311bdaeb313d4b266a9e0a487082afa2108e2ce9b88d285c9aa90505b5afcd7e296c5e2c014b9f59d93e1304f1279de0f7869aa98d363370cd083cd0184e679354439f7112f8f3e032203104941f12cc1d7b1817bc558c0ce571e6067ef570e176221631ab46040e44237555eb891e01c966e2516726a753a3932262c21bdd3223c3296c892d31078fa5f644b214f33fdd446666f58c7f7e5887d65d3846e600ea8e714f995d7a83bca04c6fe7d74617db8d42da4a29c190af2f89849d3fcc12bb70e083ec893abde1624d96bbf0b3a50df88ac4f8227cdfb9c378cc99b8bc2e5cac595997715cfca6a47f3aa895e13ca7dad6a27c9f09d5e930ee83316911a2efdbaf26c801b8266d83cfa4aeca82e44880258725ffa965a9f227a232ecbe0e045c4af991076ff17982bfe9ca40df34b0fced0455223ddad9ca51a587e38655bf29ea7ae61bc76561217fe88dc33f9b0c89506f96f0de2ec920d396d93994feceda4128da3387b7bf336e083fd1b28377033978c5d072350623ae3cef7ec70858a85827f81ba9e186619cc7d3d95987996f0d56f9a814285398192b06dbdfd3469b1bb5e82e2bbd7d1533562e397f049f7ad205a03b79028d069d471dd8c7dc8b7a2f30396ce9dedb8e824be8927af996bc000c7f41af491543a7b81cf3babf28d19a16bc5aa0119f7d1d3da91249ba78aad42ab91a0302f9364fb64e8388d9e6ffce427fd824dac1767e45166bf2b7c67fc2027cf82648f4a3beae758477d51608a39c3396d1ea067c617e530ccd78c78f43eb3dfa85292f558b644ea5b70d5c5c797f16e41b4abe7b75e5d965d484224f3004f33a3d5ff42a53ef8b983e84a016708260120d9bafc72950cead90c0cb477ef66fc35bb102a6d259807c0b7664f1292458aa212f6b5b30cf7f0097c35c917e31887b5b145b99988b4890232a88c660dd6d797532db9d6680f70f51894bdb6eebe7917b1c2c2320e203ed01fe494ac149bd84d76f2534ab19eba36fa8a21a4c055e2b8828a8e0eb72603f1408d8224c34af82cc9a8c8859c1397a8ad8afe9d817509e7c00fce80cc2b8bae85aa883931b49071389debd773454531c31ebe538a10ec7804cfc4fae4d3bfd8ec7893bd23e736692aaa5d11d31de8e5612612a5402ff5a856317ceee0ae2abfb0adf3c807f11104b876febd4de1f610972f75d03dfdf4c9aa7c2d5855ebd1a9c831ae6b646099bcaff8a8be8ce96d6e37a67e79179c4dab78f1e48b52428ad4e0491b34e875174aa04e683927d9d0be4dc7ed78c4242acae6fa31d46ed1d686f2ce2c53badf3f7419f2464c20502a0718279b8ea22c5ca2a1bd9b6d6f6fbb5b9c78b1ef37e147ca24c5da3020b09d96fe08c256e920c90c6df31a8093ae1f060e536e89c6660d4eab4208dbb0d8b1af9d8a9c110d3c2469915ce3800dd8a1b3385a42d853639a1b2132e5812f287cc1d58a30a4a301bd25c1a2aea82d2a7da8ade12156dc83e2e058ebc6b29cef951736bf07b495014c95a19a614442e54d262772a6081824a939c04b01eb4cb9562d90969df547d332eb62fdf4436e68bf71f6c2ed11a9dc8b1dc38df90b11f19ea205a527e78690cff732a361f5cb4425f56bbfd4de7be79eaa8498805737e8bae3c10d81cff19d6045e937324280473101c8881a54d93018454063a5cd01bb51142213e9b12a8c444465d4e6935c6041d29c980a7fea6a139a27a48f7bc9499991225c5b9d15ef492cccc7ac9006fa66d17190644a0f57d015c66f53fb9a9683dedcd6c16bcfb39f17a00f176379c5e99c11dc0b96710fc9427cae249122b278ce3861df40cedc0aa65b675b206b7dd794303b234dd70e26e8688092df9bacc996142988049686903a7e1d044d0908189cf495aa517b7a6054348ee7d99689f7e27e0a17e35272a05abfd8a4f538a737f6caa86564afd0a99049a3051a6fefd421d8c9bf7fa007f9a416a8aa9c4a13203bf60a603f07c3f077fe163363afb1f77c09c1327a0b1cf7f68a7e95c9c4b525ef5865028d4f38855da5625248908bf3f694b48a120436c8340b534615df21a844b433b48c9ac5644bcd1cf11251edaeca4357c5f272c554cc1972d518befbf38296b5feb50fa4ae1e96b183d3fe607853536fac47de3ad2f3c8a57b8bb474596cd82cf400f8f9fe1ec09b12a77924026f604c9eb8969bc66293bbf6288e1286f9a356b2b4651a11da00fc20750bcf1e1fa7028af8a6b59890e047bddb1550eaf467dcfb23515de47c01bc2f3e3fa00fc3b9b2001ea76d0538a42640e738e40a595620511ee47a316b6a3ccb0d4057f5660151603345515a779749d1531284553735fcd49bda57d97ec8f5b8a65165c3eb5399520c83eb93df9c177b92191f68b2bddae390ce9961a29aa8cd508a7f5d79fda5580fff6014a8af17b552cbeb744e6e538f9b72ad0196d33c3500a12a3a67dd7a72a8641f6528aa8e15ff8c807a28ac0a406bb24864aacc4837af68b67e2129910fd621a81bf728cd1c12804d57e67d29e67f2dcebe50765e0b6e5367a225fa664fc8533311f51945f8aeea77e26b8aef1d4d04928fbfa7e5383c03913b2cb830009acfd218d88d1f371ef9ce45836c5da8a89d14d80e708b08e9ade18662393fa131739a418cb13f6cdc9241e1d0ba2361289ebc852cd45ae467b3fe521047b7f2ed6d623e93e8697d27cc1257f784758f53ea54da9b4a93500bda743da8b47bcb88961994ec03bb73e8a988e0862fbc58285fe3da67a2c62004020e8ff474d0a4b2ca35efb2447a18888940307dc4b689b6b16e9c197252087d8e057a6e9608d159f15d9acd68b0955023d1a0c171a15a75fb215ad6fb540f88ca86982e6dc4fe112de7a52b53faad8032599720cfc35598ec7803d2a39be23ba25f65f240b7ecf22292361c85d486567d8faf98f63bf4f3e9eda1505a0f704b09488804c0d4451ad4ab4ab35b1ad29e143f204bc0f906bb04042d98de9294dc7cebe47d451b84a7b7cf8d4f4edbfd35b3a12ad38d9f09fb2b1ff59b80f7bfd04cd73bb71b5d00c8d91d13fb45c6313615dd6808fb932b581895c4a05266f71b6dd8b9cb689edf2f7e088dd3c724b3188ef8dc5ff48ef60dc5b8e350d84c0058179564aa4331e97ebef9892a90288826c8fdcb00346574a9402bc42290e83db3e80103e08604668b86d1971120489b3cbe23245000ad696e018f56151a4ab2549448ead1041ec074005569228372aa157568568a0b80bb0eb81a8100c99ac1303810294afd80ac1c5064ad8a71b8a8e35be1160478ef4418741d69e13be1f089e80f4cd6f97ba95403119d751d019812a1ae84cca8085aeb26f0e9697a1008414bc79a261ca98fc01e735826421fe74af6c1e8a3397eeed3c1d7b6b0461e5a3359c748b03921ae82a4431ca2ee4b0f8472e6f989a5f7a225c44d47bdc5d5f93c202d921f7980338fc86f738239a27083120c2e88e9ad20ac037f36ea16cdd59af9198d76f66eaeb293332deffedd5b8dcaa1bc09bfe4e7d263a0d8e2d06c44b42f7e976c5dbb09fd949943035011320fa711df2b39a4e76116aad75790572ffd47b48f4c6e893054b048f0085dc3f24e9036b8259f91bf0dbcb9161aaee6a7dac82c0bd216c586a1230484237bb183d2fa2be835f0ae665b2ab6487dfe60391a4e2320a93695ef675c81dbc5f31ac5dc0436350defdd9a6248491f56c64633f108c441e7bae7162358054a29f53c8d3aad0c2dcee2f6a809d1df7a9d241b352726eb891c46758cf5fb7e6764ab2e2ced277818a3e1ebeaabbb01e2498e846c2c9a25aba71d680d3d4acc7efe2455efdd5d4ae9c30aa01b1e9101ef4f3bd166a3570868c16896081327146dd78c063acb96035901835cfb36e01bb32b3eb384755b988d7946c8fb28fe79689741f824e15520a0ef060edd3de8664906109acd1899304764a09b5e6d13b05d9e99766bc560f71e426ebb5b33db253a6156b6f15666515243c629f4455f74980b195e4da3c11d185379fc1f64d2464eb4a625d5c74059a7c753455be2f68e2218933d71a0c303002a36ba5c294c50a97659c1f2651b2ca51fe0a4fb9ee925110615bf935082d2d04d14d7efb8a8d23ed04ffd4a032e0ea41cf134a5a9d7cf3dbe839f589aeaebb0b40ecd56da8a5e3407f5b0f3367c360f0d65512b94461ee4828e785289fb1dc6102ba0535517cae97441ca64684a1e484e0da27fd175ac4f8f68cf4d15c648a2ed2a7aec0073ed985ab287bcf010d2f5949db18a90566f0ead74325e202a87c17e418c4ccbdb08b5df8edf011c0498ca00c6391ce870a89c7841bc2c3fbb1b57471e127819b306a3993e31b49513170f1f1d1fe2814fcd478c0e44646939b24194e95289fb96d268a3dcd9dc83ccf242bd338a40093d1f1fdb9cd82602e3a2d5fb3c8a1befab9faa91bc1f37ad713efe983414acfa37c8c79023deb01396005ab52e64790a8eb2f12ac1aa7d39cc613e688ba60406605baf5511e530a4a7a60d370f83bfea4b8a3f1cf495278d9a77aac7e44e073f5ae80f2c67b8658fa59285f41c91cc98af6495924387ad1fe959023f666dc3cf0960303d27673ee3c9aa1b1bba7c2000a333f8f33286313d9574b9cbec767eb0f6d11d0702c347ebb6faf6feee06db67b12ed0721b0431afa44d5e77f3420cac40e73a10cc2757613bf0951d7abd6cf4be315492c65e8d303fe4786532a8990fd72281d197138e59eda4a3f3d4445823e0e91f1af81d08a99bc502f217659d916b40051216f9540a3d7e5215bc2250e37776be06c894d59e1e6a025c3de0b7558593cd7cad003c9dc0f6e3016dcf495c921210cb158d52588888b99204ce0307f71f68db78a60c2a1fa7072d8b868e1226f86d1dfc8625959e989a10b19442d74bf63ca740d07a6437b6f59276dd75d20ca9ef41505452fee7090144bd7dda99ec59446a8ae85fb75e7ab4eccb36332f93377ad2ed03818bbab0b22a5dcfe24aa38b53e24677a27dd00d1835eb6c69a42a9aede349990a4a1fbd13a687b4360375a90610fa9830f788d76d89ee0a98c2b84632d92390b89e0476386131934966291d46f3965de57bd1300e9d0df5034437af462d7a506f8f98cfb5ce32de2cf4b39ede5d5eac395a02b5122040b50d9e6f7a539c8020e2a2887e39bdcd5cb25fa860e170a638816b99d3f622a03637cad35c90d33fd05ee6bfaf6952cff0e28d3d5f2770214a4a5ffb9ca5b62feb3181f1c13f15a46cd7a6a5d7624ef47e0ec39fea64426634b60868460b077662fd9ab39e6d20f02b089b56b18eaca086f42afb142699ddfe5a4ba91fcb47f406b10b3daf05dcec6d501be1cb7a9e7e8877a3c19977ab7111d362680321ad87ba8f198e8de37e3d8b7d7fceda43c6da8cd6bb1b036be4aa729a248e626a90e6fb18bb3e3e72cf2e0c1d106b37d2e712c9623e7a3d03167eb19041bfa2b460b0235378dc58651641fb8779460dc3119f270d88771c5a63dbc36e6c778afba67029ff9d5de6f3c69dc08edfe409a93afb03268209a4129c90a8f8332141a7888ca95ce6fe0249dc1fd4b984914df68020c7430da52080723a4d11e733fa576c73417c4bb90143f4255a6fb1f8814b8f5390a9d9a5cdc1797d4af79bc30d6440ca58b33e727866c6b4be561a361b888d4f844edcf9432861cb98242ac7301941c491fb5494326aa395c64d822a317f8481ffa8f5df41c8817d64ae92bd176f22c6ae986b7a153270f62fd7ee2c0b00281e913eddca47d7608cdb76ef133a4c2c3c2b54b9e9d51d2228bdf84922eea07f51589a2aba116f69cc50e699c8ceda118757f3e67ef7c3fca2651e12ec32a16edbe160e5ddb67870bd2075634fba2d2012beb357882bbd38e08396905131b7160fdffb86beda39322b2139f9602147cc665c01c3cf57c982ce88317459606fb723463305538df827db48308c208053535d7682560a239ca198b3a792ea8a6a2c36a40fc4fb276bd8caae2a63fab88f25a2fe0de96c227b449f4ee323df702bcf4a952dd7c0c7f739a269983a04e6becde41c18bd28dae27ef8b76be8ae7d1b72d1f9c808ce89f27bbcec68c3cb56774dd834c3262ac968fefde6d0a1b1818f95ee581af8fa7cacb0dda3cc2ccd56ac041221acecb7f60b0192920568eb81c674d85592a09ab977370cae4ec4c09ac3fcebb7af4856535dcc07f86c06786014d30441251cc3d61835f2a960d2626f87567458720f87bbc861794b751243943885c76c874da90ee76b1373740c4eb9b319516b178b75b32589dfeef97ad9bd23f58c633b1c89ecf8165ce253d5b421b27ab9ca37a30f9b046506ebac01a4fe8166606047a937ea2891db3da27229ae1a689b2c2feff420990d05eb152a4b29030f28f580764bb2cb99d6bad32988c8ab7ed79ea048403a9afe0c414e28738cd1a2893be878a0ac82dc51052e9b7c605abf10cb2b80c3ba70339b22023d438d50be06a0693dd9bcc4686b00d41142c9266a37262a5a68f010a336b32ed884c4dec71e0bbacae3737ab365624b5e1025af7deb22a61f93ddb949e9d76ce8911c8e202d3e4a9c8a193dbe1aa16068329bbfd4d572f467836298a19548a110cfcf60d2f4c59e987e7c0c61b9af44da499237500cfd5dac73a017cbfd4a1b1a172b9eadf5ad8f3b625df68401fd301265621bc3295fe83e9e43de351a52dd8def6cbda7787774fd95ee436442d896ae4d9ee25fd8c814a06137f287460ff8bdcf246820ddde0aa741ad122970ef9623893634e27f5e43a7fa888a7d5302b2742f44c5fc72ef9ba1fe18d02af752c0de48afcc00f53000d0d042e3620514bb937988f28fb497348c3dc2fdda41e1d37ae7a7c1b6bd839bd0cce76f62bf45668af74766708c69b251ea191dc38b612995b5f055ec57d3c308418e5f854d7c40cea5f56747a945e87be3a9df757f76a2cabb44d2d232ed914ead7856dac7b07b473e6f4d79ded597336946632b4493a6d8d727ce956b953b13279b483440538b7e4e3e6998e07304fcfb92bbe717fd01446092341456d52ffa92d8dc85649e5beb5c98ad06614fb5871b7e5b745d89c8196a01f56660947458d14b5673e05b80a2df3161055cb7ce076018d84d4b05829679849f12495f279282914e0883b12841b5d425ae474808f6824d3f61e41890f0612dfe04ed26159382b52bfc50879d9e93ac0f93ca4476b971094e5b2565a67a4e68b86dc614b14584bdc9c280fd2a82bb5376f1759d8ab99d4ea1626178c0ea6d11f3c5e518903d46cb1ed00c775afb4a6e19841a39d3d33c26d26d06c658f33fa91d83599b26b67b02e0c79a7d8f88316df24baff79e547a35b7b6bcf0b6799b8da97d125326f6438fb65be8bd4edd640df3bdb32369bf6e5ece77a772a4e80e3005d8015ee0eb2ec22cba0490a2e1b19a696c00388efc9e3d99d367acd16fc31180478137f43a9e7c7abcbfe291c42524aa3c1487a6832ea84aa9b161b16369fbf52219e4111d584a58ec70d3f1f030ee395eea8f68a07c39c004eeb3acc42f0b24ea85fd99a116b364c10ae4af18992b50128985edb62dbc1058738f43a5e79916fc7b10d1edf49093cc890c7a286ed0d0a0a93b0efc6d4ec81d2013788a2e4cad69849cb7dbdbfd48cc5b1d2e7c2f79f16cf7320af99128179b78acfb3c3e6c1e396992a13a4ca653c5c548986ac6a95e52ea7eed18037c9c9f27eef52bd379e9552979442a4072dbcc31b2fb22a97c049c66721fe4c6ac108dbac9d907d47ff4b6df615bbbe2cb8d42303eb090ec9a143d325977ac387a127874b543ec2d11207b283fb67990a6939175457c56226b15a2e0635a116247393f80579e58ff89e9891849b50add939951813c0950fe4a12c9af872d92d14840d3a564e4db374a7a9d8d59f0c08301395e0df233dd4122e548320e07c1ad332e948ecebb6d6b50a523bf1358242a0fc00af32df2306f34613bf564a1086cdc09da2c6177b633bf201f225cff6742a9cf1af91d72454f655e5cebec8f3f72373923638fc9510c652352e0ef730ea86c5af63bc241fdbc08af021388fc080004ea8cb1ac2d97d9431f6b2610a0339c21e7cdcfe39785e85779d41b7334a6487a21afc7f7b9269b6652b237056192b06ebb82c3a3ca1bfaf784f0a483873c98e597464cf4402c5d5b5ad692a7c65953ebacd00e5e472135c5477c831b43a6234958eff77d44d6bc3aba2d8bad5308f825b40d7c2ed6e332ff4059f0bd353c1075307516706b40c8915d2d544a26bb9d94f0c28126ec9d52f7ccf95fcc8b46e54f42a6ef2149faf2082aa237c5bb41a1d5c61cc24cbf753fc00f441472d34f1b7cb461e4029c75f5089a6a5577ddafe539f03a7adeb8556e74cfb694b18f9ca9f99c9220f8109894b1545b81cbf2956fa6c8778c24e08260aef84c0ad6e0bae6b548eff55eff08f26cac8dd7fee0ebe645c4f36c4cac691bd5f18f0b3d751965632726bb49b8da4b2152f8b451f312e0d9535c342748043efb68b375a70c913fb03e7757539aeb01b033a240b509570fadc15b38630712cfb5535a8afdbae07b934f7a75a2772d5a34f981889919a2d82b68ca68ab38cacda291a924053c376f32a6e5f62c1c29ddcde0b1083a4cec6098ba1799681f0b0465e2c5e31792798cfeb840509ee329e363e7db2bed77737724ae98628a29e278b9635f41e0f32e440b1ef9d8cea93e37ba87941aa93238bd7e7abb3467d4db61c804ed05ea3a0a0adff6a126f0297a61ef765fc6ad5cb9b41d838ee81c330b407328dc05f401744de948dcde0b47295681aa3cac5ed6b0ecd5df77ce015d33a734629c0091636af64b7e0f4333e7df2a7470fc242e2137c13443cd61573c797a31a16771c34cbf9b40ad0cf7923bc0b00317c2099e15132120feb221b01618f8a3c9da7b9f6c2793b1ca2df1f397e2ea38b4382b527acf25b3df1d2471ee6b26802f1929cc3b4b5f8510e0cf4f272c2f592c77a492783c9be48c7fb917d9a01a1a47ff23aeed273c6e1849cee4d427290b73c3683d2261d8c765ec9861949658d77d9a8f09bc64e6d9a17b48e217ce2f1535b0c6616b8723111710bbf21d4814f4cf936947a8ef0ec58b7aa7e03e5418b48031114ac8474cd780629c0bfee1e0934148b2cef6f66cd33cf967b389245c44f42912d0b1043a77e0b97cf4be5c5e10d618eea02d1870b1a961852d4c8775e80f8b0d00f10d6895b574fcd97b1f5114d80451f9092dbb61a56585de5105008c038a0baf7f31e828946fad00a0fa6f046fc4e40aa20e594ad77995f643b66152dfb6e284d9e7023352a7e24260b34a5017003ce0ee6196ffbf47d5c7225bc7232bffc21e1475faa2f0809d669d653912f4b55a9a41d002b84ad676bae44ce7ad0dfbd51986f133484f6b164346a34e44be562d821c6691bd2a0dec2c7a434c3fa93979c6589f19475b04a62ecd12c57005c791f4c8baaa0a441b6f96eb66f7bf847542e676c25a5bb0defd0cb936b8cd88c57bcc9d47f1c167865595931bc781b8d7f8b92829abea2f65b340ae6d10385aac55d940e484d5490ec42b4b5aafb3e78ec1ef024ba0046812073f98b934c601a1df30b8f403c145cb11784318af7045a1242e3d1308f0af4788df03138c8079cfb4e2756fade2a4c176f9d83950230829b2e4577a7aa008eade00f942e772eff7f6aeadba4bccdae9a8ec59ac10776650b88a7e29e31d9f28d4f25eae50937931bdf22b1638a7495fd92252cb3557d23aeee96469f25976ae9498b174bf532a9da0c8362dc094967e222ebd5f7a95e26a34bd1f1582962c989e6a149e2af85d68fd323db0da45e7e2456770fbb455df9775e79214569f14e56bf5791cccc56155372cec96276a9880fdf745a997a47dc362afb9ef7ea232175e0163ed5cedcb63ee2ac13e434e7c79b5e741d9c45cd2e4007543d9aa47905486731ed35e004f0374d2843543960a7136ad6ae54858fb6f3f6ced317dfe5b6aa230862650aeed14d1536eb3219542ea788dbc7dd93613b3d1c1c6d2712505462f47ada1189f5081680ab1943001a72f430b6ed8a63e582bb7c390bd3de3ddd4013d958a20c162821843bc065f182e87ea09a2d70cfc41e1e629d8802a717775980591862628cf8eb6020eca3f03feaa8062cae4a188d0e3a4c0707a6a335347b9091b26be08bd409ce472be796be12df37f7f17167958143e387ee5aa5f592de6f2249c0215fa0f5387ae021f4f0a0d71c23a50031b314b8d308fe14413854146f0690e60c1f72564e5388470589378b79e70cd1da0d9aef4a495340296ee77db2e8b9cee8107fc7130292605c7d2a41cf2d2cfe76b9e842784b699ff3c394896784d8e878e8775bf7fc95142e2176f233da01b2894941818ba3037dea6f48df58637e6dc59a329116b8a4dbafb10d7793889f5f3f06b819c596e719288bc24bbf514d2e745d9284fe91adcb5eec92c5ec134cba4b123eaba5238300f141fea173d5f694c580c76d7cb33b2c8743da7dd76c02afaafbded8833304e623d48e6ec61e1ac5e991a25fc11987db54eab18f240fd3adfd31cad90c145e97ec06e8f6f65ca34d65ef549d9949b3bf2a608b842f5fdd96b77ffb8c9201f7e7bf8dd5a93b6f73312b9c225a547fb4e7cd94c69f36978e54b36a8f6823e60b2eeac897c5578ec4110749f7f6b5cd5fd650b3862a6de3a53ed7cc91e06885bb5cee71076b340421631ed8635a59d00568b3917bb3ead3ef730841de2df2149cd603ec2eef0cbcc230f19c01073a0303572c8a28c66b1ec0562013d9cfe5990f034b2c8d8b1c6b3be2e3c4d2226d0bb75fca397fe03ba7298f08936e901f5f6a1d2c48fd04261f8deb8a69d7ba9beee2625890bd7d77e88b79b1978ac1e5d8808e4d792ccb657671f21326887122f171fdf513919b18c44e5f3737f12bbe749b7ed39f975409e29b73a29291e795a68ef32983c5a6d719e8b37da94ef5ef056918ae00f237f55e7eefff95f0c63ee8c5333c2ff917eee9fd35e3321c3302cc308c5bfd7ffd1f51ebb1ff34a96d87e9ee2ee7a06f2403975c3809573d64f69a79c7396e17737fedf1dcfce5a50e429d6a962908aa21694bbb40844d5af555694d1025398168a9d182396e8a97f9025c48fe1eaad64295c2b1106f7333e4db1bd5a962fa436621e60545b11f4e631313628cfc83ed7b3a9f9b57a571a056910c4845ba1c053e370db614ecd21c35de0cddb5fa8949bf6f982d9d82f432afd6e8951b11fdae6f646693ec2da23ecc70c41238bcd40a0b868b1078487a67306d9397213214a1ad11a7feb1b496c14fb8e346b89e8275d07da0382995ea24d083e4a7b8072614f2e577afbad1bd6364d73e53386795cdbb68c636d9c64890fc5341572420322ba16140761d208bf60d32ad62a34f158c501d0230e2dad681e2515694cae96b006beb06060fae759ca1944a41143d953b71381c8c02f08356b469158f6ef3b1916daf6cb869e72c93bfbf39efc7074ac1c2e75d1edb7f14ad3bdb63f23db217001f7dd09bead9f13e4f5f79f37aee7c3e1ac07b7e0a822fd8f0e8375cd670c073a4fcd69558645ed5762cd1b231361050be82b5c88558613f9712cfd240587b0b2ff9e425f6ca028e2223e437ae2e3230f31b099905cd97b72e51b72a1d302f787ce585ef7e508e4f0549a477c903d8d03ef5cec6887866c5dae7bd13225bf6ca4d75f5416c948b7ef6a5ecb9baa674f5ce6943b87ee45005c0d1158e747a5b2dfdf542bcde580f862ffa182f380f2a5c64461e928ea9ce08d6e56e16ca56fcbdb35fd180f7985fece4526656477855825e3f894999b927514c9b19db19d3976f43029667e13b899cf0b55680fe090d5aaf3f6d170605b20502b9e9288ade22afa2f681fe10719c5a8a90a8e0942dfb7a3eb7a9e67153cafabcab2e7f9de77d3ad685aaaf072390809b939438843cb9dd3c42fb880838baab19b25beb9b1558b857e5ee6ef6f9d59d76c670973d63d3f011387a436e2afee24a39fddce92f6f8e96cd68a5fce190d2c5c7cc9200cbc537529fe1a7232e20f0f843d70cd4d5325e09b14fc4a3e07b372f5e9e2f14792f4c4d89ba89afade372b9ae3b31d5b79ad5697e4a3cfd7880c803d481fee2a43cf0145cf7346da969374e8c4fd3edc7da1b3a430e52bed898d0ef26e8f0a731a907aba6ca5744b790adfbf39210d7f8acf24b3ba2feea05bf9a7c18dca11477c23eb992f4d086f9a4547df60d139f67ee349e1092ab4c723b494dab78ddab7f7d2b7cbd2b758d2b743325939379013277bf8db66c78ea56e9fe366d6b7164eaa0ef52639a7254ebb9225b0e4e384608a59f5a8af2dae14eae518e8838cbb840cadbf3da50d85f7167849454853616d20d5384c953da8c80cbf896d2511225b41b8c42c763cd7351928ccd8df5b3f4c4f726ee8e8f273271f137a2c4021bca6e0daa2ba852b58858c61580ce923eb0a49ce6079264fa8c2a91be7ba14d6139a8608b545e8cceda654af88d7e16b5f48434eb2e03e3b3f87df9d5fb7a83df7f066087e80158edb2066a726c55988a4df3f35e7dd813d2a11daf3b185a50df531a3be203ad89b9ff4c3b50b319c24ad1a518d94281acc6e67521c2c9a199436ef03e99c826d41d761abf98de41318cff85aa8ee9ec66e6119fc74342496343025194c774ec1552066df824c23715bf1fcc3fad00a5b0197e4cad4f7efd1e98d1d9fbede1a75b6bc0b87741811e6598450402c092df11ca833e089cedcc96fd17ffd3574764b9be2fb5dc67766d9c7b626cd22009169d4df63cd06b47a4735c3fc8f7f55f336a6c77fff8f7baeaafff81ffff5dffe9ffff67f55f374fccf2acdcb7ffddfffed5ffffa5fa5b11ddefffcd77fb873361ff37ffcd7ff27d88f7728fff35fd31fcbf0bfd1bb6cebe6f8cf7f6110f487ed5bfe9fff3ab7e1bf17e991fe67ba2c439ba7473b4fe0bf6bcf55f55f79936e7b79fc9fe751fd4feabfb2742f09ecff28205a72ebbfd0c7308c94ee0c6bfc933d71e7dfe1f2fff762257770cdbfccfa4ff1fe4bf3c48b72e75708ee2ffe8ba3c21fa8fff1f00acf32f2eec2acfdfbab5afd81cdc8304cb81da876ff44f907071cc3305af4a7e4fec995b34fd950ea695fff1486618e7f8c047f2289abc7f58362046ef2bf384ed57fa0fde709515b7c5bb35d31397fa0f917db191efb4b01e1150c4c4aee0481188641fe9c64f73fbe31f00f0e981244fcfef878e81f53caff5a23300ccb4e993cfcf96b070cc3f87dce300ac99492c8924833c4a1c33022f2a7ca187faa7c6c99f83ea45233c40ec344ff5e58fcf9c3a80ca0b2253e67918b310c5bfc3361a63f91b56ec56303c873c588c030123bff597efff88a8691248dc94211fb331277ceffe65318456152fa8c43786018b6fdf38fc9fee426a21b84784eec95a30ec348ce5f7bd87fbaea0493500f815c56873fbea4fa33c2fd734e2a40e03974356812aa23c3b0e3f28febf13f33a4a0dc08fac7ea3f43cb7c3cd27eff74152f50ac70fe039a36a609e6fffa9c666b8c315bdd899d5852eb3864fb95d1dbb465fb646d9a92497a1b29303689d9d7371846c400d6fee366316441e2126503d88da40bb5fc1173fabf9beb283dd22a9cb8309c53b3aa7be790e23733f3c82ca2892b02cb3aa4fb1dac27dde44b42cd3abba8edc8b0373ce4b92876b494db900d1c64a473cdb7105a523faa8a3f2e6b530e5558a1c8d94af864dd0a96923819d96775d2d746a2a8e5e4562dd754cbe5ee557a27eeed0f784a93b7800c2c01917dcd18de210f5957dbeef8eec7182e20378f7bc9360d721289d00c668bb247e79d1f5ab512184e8df1408173cd995a8f756e8cc4578d9067c28d01596f805869f86e6d4c4a0cbe68cadab75c2c82adec5a22197ab42b7da0295a8e72f9309ffec144f3c22102f69d839d2fbc15425bd1b115a7836c5ce6c9f2dc4d425c5f3dd8d324c7c4f43b8130c3f1eb385444cf706d42828fa7fbc0bba3e3e09cbdb8a99694f6fc9e86ab43d36db6604fe262adcb5b6eb17b4bccf405ae37b9bad160273bfeea73e2e9ba98b7bb4aac4710495613899bd0d71ca74d7423cc1010033a8f323dc06d50ef065fcf111655087ad11bc6cc016e9cdd16a71ef0e18cb457483cbf4b5d6ed3a2a2bc3b801a365c0ede5aff546b5d543eed0725506da9afbbd4d34d9ce3334b70acfeb5fa26840ae52d63ba954904c2084ec84ff78bf207950e7c7eaee1407220f870eb4934f549d5968abcae5dde7e779a7098f1765a43804543faab9a349934e3aa6abe18a5027083ef4ce27462e47aff609f78f8758f7bc34b5dc3470caee0db31cc5eb6742e5f189bf9808d2c8d4e3458c62b76630aa7e3fc09367b5fd310e41e500c48a005a42cd065a813157cbb079ef462e708c9559ea3a0045a62807f97706c256e47e9f0ef660f65c369d489f4bedd2b8f2626e5b7a4fb3bdab79fbc9d1e4e0a3f9cacb0bd3ad27b2ac982954601fd4e93cfa12848df7eb9d7a8547eeb49b4c5ce3ac6cd72de290af5df18b7f435cbcef970ca992c180beb544103fcaafd941bd62c71d57253024e65dc96f928157cc3c22a6e16346edceaf6c84f09024f4c0faf0e8e42b6ddcd69b9189aef483e553cb562ec9315c337e02bfabdf7dbc716fe4bcad684bcc885cb2722871f4c810844b7bc68c74d0d06c32e39127078fa95bab12192e713b5b4481adaa536019687bbd082b858cfbb28cef3bef15a3d8c62b4bf2b2da9e19ae41e87c441f98edf6e8adc1907a6d8cefb28a2dcfb613af02254728a3077e77f98e291a128d5272cf64a48b410144a2d04495e83c47aa26650be4f7c7c37829e2c159cef2f87c4213d8a68e291a7c8d73b7e0e346954272a9256557e3279e2f9cabdf90198832787d799a483ffc9a1b6a725c65064cf652313a86af46aa09a5d9a421e4329b46f6c17888e94163df268e59184c8d4a7280e496248a522a2d6586a23f293ac06ee75e5ae7b1ddfbf171ab3a653425be76b4a2354dc3c408e42faf2da93ef8d8412fa6cc1a7a20b97cd49cb8196b5b6f5d797d1b94dbf37fd9dfa5eedf97c9f0fe0be3c4792a4aed9fa5942ad24d0a9c7998bd243c207da43a7956a1841e4a713f933d107f4d14f1a82a7ed440348b6d040e7c07cea40b411e530345880e9f8e623718f5f3a104334a25ac54b9962331fa638ccc7c93add1350e8d0ea19ca5b952d12c55e9c6d790903d8f53ce30f1fa915e980fc7c22e0eca54071ad7885787a9bf3758b4f5a651c487db319de7d00af97ed7eb62129d05a4a6483e417c504f073376c6675073b9a60dee600daa0f9f162c9429400913ee80fc0362272dcb582b06a221a28a4ee62e9175081be797cb98322fb330c809f1040defb6c8f9875f1426d49fff6e4d203c81b24f56811ee4f6803951f47404d3e3ee4c65fbe125da8a02a032e6022eafd5ee1d7db3c784464faa0b4617897273937ddbebaca0abcf4664bccd6547bda77fae04d6f7c23f62b5f74913dc046d211b8675be56ae5433b6e090aa7194b105fdd19ded9efa27e87fd765303ebf388f3798ab71fd9d2caf975bf71b92d008685b443f03aa9dd899db419b9864dff6c4ab5eeed4624263eb8058189f71b81146626602a1ce46a80be573e2aba30be6b7831bbeb0a70285a183bd0ccfaa36a40c5d4fdce04f56762343bbf8442eec2e71a15b4cf08c26cd98847f9709aec8f7f5066813b3a977e75eade1958f9008965fe229b54a7f3134c951bf84cbb9af0a78157a58c4b034c0127aa9a3cf8eb9403b84fa60581f3254af06a35dbfa445a9b8a8fca3bebfc70eb7c860b67746844285f84e127e8a47df58da98d3293f261a520aeea92e1aa345622346c9dd896d89715aa949fe06744025d664b3f85267247461411a8ecc5bb0e502cf994c418f8d46344163951bce92275856e442161c50fd02de6f39ccfc618373d164cb5b2b9c22d761c771305de9e5a89ca9bafd6b23178ba65df88f53139040fa00a4033a26f9bb000abd8f0937c4016e7103f180fe0cbf225eb107275c2d601378d20577f4e04624cd01051bbfa5243dc132f6cb76326b2361be6980bebb4aa93a49b648c9694bc0ddb24840ed5f5d9d3363367b7b79c6e997e9ec3a8518a45c83451c4be618f11008def823c12236c02d0a9cc561cd0a66a0b740fdcdfc145c773f3df041daad3b97c661660f8ac42f985efd324a938932e246be6404425adda90d7c7909dfa13e17a4a46664e5e98ec8b183815f84938c4709004b6d9ed07d165e47852430f7636015c42300e143ca5b5065ca35136170f64ce0efb6259c4af0ac992412c6d6bd5c9eff9fc20f73f5843d88a43a6651d90316dca09198356f4a082a68d1163c4b43eb7f9f7405f209415b1b40a9fb90f30fdbe55dd88c0dfc1aa8fbe15539e756419d17d6cf0a7ed48d8466bb18998c759bb4c74b300c01430463f9fb620530d9e9b076a078cdb41f8822330023fe8821cfe05be15e0be1af8699b76ebb6fc1ea45b2d05faae042a8f3b26165f3eb5e0a7083b9d6a2af1e39320347adc08c82f71d14355485829de3ec6cf3642f08950abecf157cf427f3de03eb201902e09e12272142dbd69c5b9f4c050dd557e54185effb39dd4c5925bc716db86867e778a2c3e39343582b09e99323790b05d5e0d8f94b0aaa4d07794a4d74e3837a70c5677322ac64fc159b94d3feeb67e29ae755c6c39a5c80843553d544d9479ccb61e0fca089b08cddc331a2fc0b5cdc1b52d608d64d44273df6f1f26969fdf7b2c9a9a262c4602cbca0b3d46421b5c1f06f8923512cf0775c1f11c1bc4e6dc2bbd91ab5a50c708db41024b7018cfd6bbc4be9ccdc4ed21294abc37752a69f1b329727a38a226dfdc164b1ec6f069d497823ad89850b85425282713c6b7718d0ceecb0268ca42b372e7a2dc5b6bf8ea5d1bf66a155c748d0c7905bc10ddbcc8107cfb09acc78cb7c57c2ee5b24d3f87f163540c83877bec3593765bc2f1b6bca428782132cc2fe013e8dffd1dbbc9f231b7b13177b7ea70d2b27520944e675689d074f1c8d68c2428a5c133b40f304e2694afe8085481d260012532dbc6d566dd4675f354376a63ac0412db4d35e0c69b58a30cd78b6c1b33febd1b90c3fcefdf25b1943231dbff8db653327a6fc43aae33f754eb4e59df9af338456ff8f26d1cce034ab991ac4f6ded09e462a2eefc5cd6eb45cbf90dacff13abfb3737d12899f3a2af134ded2008f0bea32d01c2c6375b0b410ed48ac6d4b3c7f04148a901452981012ab7dd8104484de00d823af85d00063624b0cbea83e5d5c4d77785c9e543ee95ce3f773ccbba43ed64030074bcc152e1152ac34a66cb48e5a32a062616cd9a32752c89438c14356ee92356461d4c333fa1895dd1352241f5bc6191821e2f9d4054c35f10f801ccffa02373e1c24629de79cd4e808d2fbf0f5d43f62cfc0e9285f229798ac84473db7fe94b86290a547866b63807d2f9bb35da9950bbf8b45be16953899e89637af70ff79ef88a327c7fd2195ee9f2444816c088f840c8e444c9a123719c069e51fbd1d64b0ebf53db1f3c5c4813e2c76c8e3d56490d8ed4b73c9cd3e0c40f8feae5fdd237f2cde1c3b8b1ea8c329a5ea0cc44f347e6378db2fc6606f24bd296130d83990cd185c0b13b0f74f0b07ae30754d648993729d7ee4f34608131185323b4f59fed0087603386991c8e67148a196eae61b41fb3dffc8f311866adb993d17be66678893177a663388fd17766acb9809173a6aab99bd10ca6a9b989111c5170fe799f14bfecb04248705007b2194c16108ec191c6fbc684974a4210b9dc0fba50ed2d2c1a38a947f345d27347a81c4514f862bda4946b16a1cdd856883cf9e1ec56f00ce454e6ce212dd2fcbea5ff3c6a75d7a95a528af6f1d773ee03923e3fa3fdbe85f79b4717ebdabaf885eabb48332983de2922c17a97465afa59c8cce03e7c690e95f8fec9869b3781874c717346822cf8594f0b99a60dc1fcac129e20e0cbf178987d493942bc3748d447d4c41d732f6a3b622d062e830e1c9f3fa5fb791f89ec5e8af109d5a49a02eba708248c17f9538ae8cb5d57ef8d73dcfd341e751d50599b6df2010a020bfeec2ebd03e4dd81a6aedfcdac9d2abee104e759cce2dff8076d79ffb6b7295449bb528e222446ce75d3801a13b0083ceeb1ba740983660196743899d4171533f389bd83143542f0913d2807887d7e472528fd7602305a09696a37ee8e568b19d5155429a84649da015a67b2fe775db6a37a0c5e49768543ca6191b1cbbfd111fd2c238895722233d4d6f5763bc2332774672aae755c67a89d4c0026ad6bcff9a9c1f41b12bb3bb96c00c4b5ba9a295dc3a094c104079dd1f202daa35c9595bc237b532688049eb0fef76621c3c82078337669ae74e514b568462165de0b8296dffefd6824bc3525e199ac57e345e90be98905d771849fe3f56be4e897b3796eba9af1288efbcba8c3cad677153b3244b93fefb167a2b12b1f13f7dd04bf0df946085288eb044910ac0c6ada55e3cb7f00b9e0f06be12fd95b3211c36aee5b9c196d8d76cee3f5d3ac849261c5840ee9fcc592ec415840e9a97010a6e4207807bf1d0b2002aeb0b9881b9c58e978e899c95c400ded7f303d57b5c562606f2e283efcde40344801c91afdd45a17a396df5d288e5999b914c8bbe82c68c84b72200ce7ad51e94879c93b78dbf61361c1be971399851a172e5ac831d8fb147dffa544c13de7a6faaf953f22eb9137a4de3f309cbf059603f7a8f2f04e101dcf0efcb22c882630db456913fd5c36af4c99cb5f75c4a3e5e302aff12c21de27b1df7ff7ddcc43bd8ab282a9478be9bab0abc78ca9f41d4ddaca0e4660905a9aa083772ccab741bca4373acc210c5ae18b289eeed578a8e7ca40b2a4408f96262d94e6afa9f9507f5f9b3fc37abcd90d063a9dc322fec759ace40771161939d68f2334d0d19d4f8935fbc09e8f2adfe565350dd1cc53bfbeaf702db7b2114ba97e4d6529d6b3180b0ae14d69c7a841d9383b6e60de8998cda13cfba954c661d3d20946a073fd44bc4ad37754657ac8448e7dddd5b4c44a12826f2c5558e9ee470211516ad9c15b2be33c31b9a227bdccf7bd28d00a187f5dd4b6c9d7cc0004b4d253f0177bddd646404f8277f57d96fb5eb57b363d6957c05bfdd13774522f58922c09d66875cd28a5811e91015c82c4c3b45845296016d8c366762a1705720064da2a77b7f80fca7e8955f5f812a2800a7ac663ced88a692763dfa29bdd1a709bd00ad4d6cd5670c56d6dee527d9aba937446917018f04bd0f4895cfab700e70e7af6b15d559241c82f314464047b32fade14d25f2f390c193b603e33602cf58faa16694cb239b98a46876833b1938748c84ea95e7ee26fe1d7b38558e6105d695b42d448026112c4e669e8c0382dd478ccc3429c3b47579cdb5d3acc9fdbe496020cc5588d2c070bbcdcb90d788699e47ca02d3e8a739166a10417266d74f06b0a1fd99929c5450e09a033dfdb63157ca3ebe3662d0c8baeccda1f3d7d170cf2e428c562c257b033fc5e4533482251b5d2712545b710ab6c1446e0debb268e6d96a83895a2d8d6404736b0b2712b8e311fd3bdac2cffda48824aca28b4b36bea541a4391cfa3ca6143a705700ba8a1ce8f0baad2bcae11a0ec5e363ff0b9ceaa9b41cc27df0748cfa5b8dfdc9eb2aef946f43a071a082b22c55ef77cd4e95ad4c9ab76d24a220d8fc4f334c852c27ef5213e67ab8cace8bb79160f11fb3bc47219327609df6120f5d8511d85bc80b886e42bd7d6123713df173098ff8860ffd183bffdba8a5b46dfdc45c8521fc67243c3db2345ccc9611fcfa9e8c5db4f072b707f811ae97af7dbc48f0bf4dd0c8a1e1376c788b9b8a0acdced3836fe05b0f92b3c2b7862263541c957ae06b62ff8a8f6f91222a13bf637074b2f6491628f680ea4df88ce86e6b9611ea05df9e1d8f16bdc16c90e21a52a6230418f2455257bc2fd9ab68926a231faccf54d8a24386834a5496d6b6a003c3a31934d70a30583d41704cfca8bf390e4443e9d248e53a24f98a633ae816f93ac738f26f35ba5dc629101ee6a98e3a3c861b6fad5ca04b7ad6349f5b6b0630a3668cbc6a39ebb162bd5f1b798d5f7e15bcf90e197f37032010ffca2655777127dd5923b4e154459c3931a3f095ee192c6cd386230340d4b82c86a05e7d74117bcfc7226baa385d2f24448c3ee528d0f911f31737b92710ad7aeb59d29805e28b4453bef07988d816f945dd35c656d13a9a6af6e0330abc22ae53c9152c52a08d90a18ceb0ecc1ee5e84092c29b7aaabd105acb6a3a24b08e98b51c7817673e87d8cbaeb724ba919e0870c99b7c0dcf723615d47a7b571910f20008a1e9703985f3321d37232f96ddee98d44e15937c7d05e7e9529774be1a2449181f5d309391a9f687b0ba73f0684f55ba028b97d37d5de492ada131f14171dcbde24d01e0f4ae3c700726a1c4d5899f0b1bdda0d7556327c39e43ebfd24b6507515cd6398664ec9111acd9d4f6c9cac85ae8c290c406e9907ff60e1ef7adbbec3cfc541cac3790c8334e8fa2ef1936315daa428c4e0824f3ebaaf30ca914bafc0e37e329e2f0a797ca8313363f00ec9999807a555da337d9bd7b7fc4152c6eb0a82ae45237969ad0cbf6750b754b6ab0174e072b5fe3703b4e0a7c4422f742d0b44ac962f1074afa76b06f9c01744c073b5f4524dc314c8355be1bc4f09057a9e3c053175ce151f87770a1d4bc1c807741e2613b76aa4034baefdb1fe243e264b01499bc64d85c04183580592493f0fc39f6b5888b76553ec2942c27ad6433c46b88b5a4f9c2b062a6f5bb3de65b7b9a30bb30afe56eec54b3cf7baf56dfba19686ac6730dcdf9dcd26ad28a601668b857fdb54f7fd5213bac0bd2ccdc5cd76054f379c302defaae33b14327b16308983859b28051320fa57b347b9e936ebbe03c264984a6fca03b307ed274ba3d1bb411b3723f7673c6860e6c96c594137f9207a779bda39efcb4a206b97168a6a815bb4ccc2229bd1820f65e0dcfb94d40f885fb8f1f1d43d18c0f4e87c75e29b774327b04e1f47a518f92c233c949ce4c04cdcd61d57b9387096115e3f8423425f359e7ad54c5180996c47e701c436cde9a9c1d2965ea240b2c1a4fe3334495661d86c1d8dfbd2c9c76d41b98fa862c3ff557092644a1074fe3ad397f02c45aa1c05a88217384a18a5bd5b448c2397bdd08e680b0ccc7db8d36accce9f784a97a473cbb6ffd04fe9063b8ac9253f4deb55cf8bf203f201ade2699df0100b0b8bcb04cf9457290e480cc12c3df5e8ebbfcc1202d7d6d591c3f3b4d41d0a3c78ef4d870b16499aa5f5a7da20ab4e4d780c29ad46da623a2c9bae5689a1b5dd31f3c0c7253bd988931667d29823a901db8b63dc5f61817bb197f13da5f2dd594d75a557df7dc5088428c48024952eb22bc60015cebf2521e4fd92fbb5b11cfe86725518398392d2e79acfc0c399d5b824be942c977fd3d793acb43dd6dc1bf241a35867e961e642c5e2c1ac73cb921538c2131d82bf181daedb84ee23dfe4aa7d8f1486deda5515badb73766c0b88571f3145529f3c2533c1bedd6064c5913f3e9f3523bcadeb2abae7b5d6e24d14441f004679b74c1b44b8f897ad013a51b14d475b87d1a20eb86cfe96a9578c131997457195eb0559ded5e43a60ae6f0fbb14e24f3f610cb68c4631f2275e15c3fcee0c77de6f4f0900ba3c3da9c042bcae1463fe6ea7ad7d85be9ed8ae6e42516fecd6167bfdc7ccedae6714dcafc6acb8b86ec55b04959901c329b900d24837d95c1671bcd41e705aaf1a62a7f5bb55e5d6f6fc109e4966c04613eeae9685ee0761eb07d4dbcfff15a6ec214e1b06fbc87bcff81d344a5136995866eca60b50750514aae7b04c4975f00503d185bad183b563107ea1e370093e9e698b2c5cb177ce6a58bac644490c19b79c0164b6f6b64ec18ebdd369a326aed62b03d678cc97a268b932aa6c32896f3130611937ead080a3a67340d7df376ddb0ed060811c93fef0c515807a0688f1c32e763880b17cb879b5b002a02e22925bc2b557ae3cb7bb2416e7287da10772f17ca2cff6ed348a941bb1187fc7e2c35198bf788a473eb0711f3872dc5040d7728f082bfb8c7b6e26dadb21f60e44711a29cd3cbb7e57819f6cf228ff9b94b1a26821c94e668b28beef27afa7a585231594ceb7a6439feb05cc31b5618439591fe3938ace6da53bf4b44bbc414ad140fa0e9dc9ea430312e626b5b13a48f4bcb8a571eaf0abcf43e4341f52aec0ba6f1421d52384d631b7c217395e4e2d23e0cde28c358cdd061bf63755fb9259842661f5fbcef33147ed2a821115ba6091fb30e87d4b3bcb2bc1a3b40bd248c04a50e7b0a133c94a10ef73914dc7036cc06e1bacfa63a56d7e2cacaf18eef8c22f5042a5d1b49beefd04ce7e2db6114f34d63c5131e0c52c089b35b269f7b3ef9c3775369c18db64e1e6ae82488971e224a81767f91f45dc94702aa40010add899767ba202003433c0ca9a0e808076f31a414bfecfc1ba986d524d59964b93d61ab69f66517de24e21a50982240bc06538f50595c0fdcb3e823896fa5e4df456110dbf1cfb48e8c440b0c3dd0780779e6998f78b65eeebe44e7a75319e58e73cd6ebada46b5a7bab158d785ed9998db6bb600f714fd404627b88209abbb92001c3675e0fdc71ef1d107bf969f88df891880bb13d0769e9563c8bf2117e3ba94dfc66f5c347ea8dc46fcbdcd50188500aca8bedd4069d28609b2b252438c81f21c2b642f7a9d6c6e7aa05e9026424bce521bb9d0a6210f390340878a4355477164927d1a4c85e14794bbfabbe44fa5c1cc14f3b5b7a4c9ef3b50c01b1d704f980b97a860f1d961b13b310707266fc5724551d6ebc20b0b66fd9442b1d6c761d6be01f70c3fe57b990626f375210d98a0abb5102dea18aff622523cd1e783949742818a6d38be9b6686f1922664f50bc4924c6f5cbbc0503f7679faaae994c8a1bfd353c7ef1b70160df3d00185483f3adc14687bba30460fb8840da28bc246208c3a42b281c1aaa48b08201b8d7f3e872b6db1a584a9fc46a51d52e487df126186f4c0b3692a3c6c1b9a8dbb522947dfa775f46ba36587ee90a51fca4478799104e9e36313235068b7472e1c4021b947feca2ea196f8bd0a2e37042878e1c155f8ba9835b4c5cf9f0adfc64e9f9be019f070f8cde40e88f672c6840eb0dea979534e24db3c36d547354862817e06a1a421ec8530746ddb96acf5c7c9b4fdefeab337e2561973c007bbaefd35c6b05af608557232ecb988134a8196ce4b4dc4d4d9632995fb24beb48133bdf2906fb54a31b7ec3c4abdf2d6f3934b7976241b7d22bb01c8efe635dad152ff0aba88c29f245002d9c05422bf53be6b402929dddd77c9246524497516c85d390712118f6ea7dc02717765ca71b6192a35671bad9b7a059e2f12fe3bdd28a6bf561fce3298d75212acd51a6f6bd1b6b121e6a1604a34225f2bbf0d6fbd15bd8a50503d43c85c6e76d3eca2463fc8abae0cd6c5bc444038218810501688ba9bfa00b22519751f84589d9e5e9333f8d675219740493dbab7ad6f59dc03d3b457207755e448da3ae7397dc74c2bad3adf3144b5dfe9dad3139d1075dfe262962d101d1ff909893f4902cce24f93aeb4b830f1106852d83ab0f6dd5bc07f0027a6ab725d8c08b355a8dcc2e0776105e4fa7c07ef9ce25bb1bcfecf64886185ed255ae9f1fc51eb5845f79c26190c90e7de36d40703cef5b2332319d7bdffde9d5c1002c23bd5f387715d1be17469a8ceeefea5c0ec098a3bd3b41f1f11cd9b30e1cd0d25dc2be6e48aae2e6bde3408a43b364d23d43a6e31aee7112c632905304be250a0f92a28d4bd4ba8399ffac0d4549ff21ee5dad539cf54ff93503ba65bf67a2a0d3c24754230a87f497eccc8dc2a4a6226f4f2a3215e4591d38b5cf8fb1c95d668507ddafb39b720d5af75a485cc4e8745f3eaaf3a4871fb8109e42ea4039bd57d74e345140fec7bc64cd2e4370991669cc6f4d92831a2a94bd3998d7c450ac028fa7d8b2984a21c2a28891114dd267c9250e48c901f00a6d4851ef561694c5fa8f069d9993b021cf3011e3ab16f0cdec619e63d374986a8a579215aa917095aaaa9617ce78e2bcc8e1cccdb8f935c1e6aeffeaeabf9e49984f831b542ce9cf36555f66e55757d1c1057c4861b55a596c14ffe5473a482893aceb9bac1ea89a65f009a4f593c0eddc4309f3a271cd0b82247b47dcdab4d62eb84aaf4d8443b8c73de333d84867e9ad3435de8ee77b216e3e28b970b86c3cf2b6ccab33a0ded4f37d7c537167f4ba0fc18256a7525323fd57d6276c7810bb18b1b200b9244ba2a26083f9b7e25fc6d09f643f582805cfdf0a029093e9aaa82b1a667919e08b8b2945f0cd3189a1eea8cd886ef8434c0c93b3a9227ebfa4560527cbd1cc2793cf4aede13cb787b9b2ef63ded88317ead5c9f86c0a837942a5313753465cce1cc5f3fbc76c3c137a16f8d1cb6446520e6a79b4f68dee6c87ede5896b7924f78a87168d0aa977d109def6345e3aae2108228a7fd0aa6947229c88f4e3f22e5590f747c613390d80618d9684b07f11200b20631cbe98ddade2875d0cc3970a8cc778aa21d79f4987e75ad8a3cd36c4f734ef92d631c08a0a5f468833c15bc85b0353ef28cd483393408a257cdc50337a5c8b344691f6432358186a10a20fd3a6a4cdd1d7608caaad882cc591f94977bde41084c10ac4cbce454089df0e7130a8ac65f1f8e650947d20ff5e4988c839278740e130df7ef34cbfa59ea6bf32fe7938731a24f01af6404347cacb231df2bc6fa08f62a833f3be143e0b8f532051262bf72e42a8c1e8acab4d17cb23c6292768b34249f7bfc98099d7f953400b68e5a7b07d11389cf9c2d2bbf92f36c89921e414df39dc3f65c921a720145d620cc0f07cc664eafb6fc15f0ec57edfeba4c021873daca676f94b992cf286ccac1fe484219fa69c169638233b20c38fce3dff5923f9dfbcedcde3116c702cbce867404ab953cc153e676254c4d130951c426ac99535da486d24befc10271e14955fa2fd5167c5ca88e7da8d6673f0180fa7c23be6fb661075a5a88a0ac85fb686abf92f2b6fe0c5e596abf17ef9ff668bcebf257bec99032b0deec52429f68bdf0f96a5b77f7a16c5669930594e938f3834d6d022e2ec33023961740ce1e73c938ce713ac3102cbe63a33aadfa49553fbcea104d068b7da5f8a86a26206bc613ceb1eff72ba49296458316450b3fdcb88708934d0805ec1442f3f8e34cbd2a64a06bd69325a8504a72ada82791894749c8807ba42aa4f54cf58136465ec5b001105e4f5330e230385792084b52662d5d0fffc34fd631d4d1305bb3553d1ca5dc0a2f412a21ef875bbae5b6a9c3ded047b0e97e2b13959a0ce4120acd933ee152b0cab915f0178befce718990323db6a1197de93724da950940e64d063ff3aafc0909ef4634c329c96adf0cc6857ed59657188149a7744bfc1123350e7b248ad0456f851b96bbd751825cd9773e27d248c65fd1cb82d17c71723b982ea85ec31492d0457d8130ba394d044dfe3be891acc7ab9381a45dc1311bf522b8d3df9005871899fe10d41aa514eb32e3524fbbacd83f6ea3c4e639e98ad1c02e131b95bdbde8543bd18528a57d2ef6a91dcb97df0810195cde1b255c4a68718aed3763a8e77571876683f94efad45e2f318ec4fbf2c8a717ac47bf9e367891ecb1a0952decfa497a39aedbaab02a60d12fd6c1a38b0ae7e42df476bd9d9d56b84795eb0a9321d865d300c96c4416ad5da81b51a2a907826099f1c8411c08d4f5186da779a28432f413b02304008c3a3fe0582243308321d55cc68445decc880083b87bf265ba9c43c1d29e741ce7323896320617723df2c57a451d2a0db75314e16187da4328b707fbce3f0e1a80b2d2978e1f45ed3f3753df09ac2e8bdf00eda22a1a2b6dc2281c6d8f4127691187a9e489954958a389fd13ae4e73d05b241c95da19a6a208f7d163c209e73a45997c57bea128368fd4790d839637c57e5b1ca63d660a9ba4cc96f580772ff57927350183e19feaaaef7245e62253fee0dd958a86944e922f6460934e165b79dd14b5e8307ad3a70f023057a9a467af36a61abe4de97d40d9fd883a2690cd9d95b9999485adfb58cf226a01e24a03f11bb4a98918f294c35fdcfa6697e9097bbca3c72bab5f84162ab596264d3ff3125087ac584f7fd87ed6dfadf43ff0e13f6a99fa662ae1aa5890dfa6f60810c34823aa40f9dd176335003767151252cd45234fe6e1de3d65270a3330f64e4616b911efc5836cb3f826e435c7af3e65925557a5cafb0bda13b9c7713013cbaf95c51e50ff6e4c9702ce9f01f55266b00acd1d9a3fb0d658d2a3e53c4b5ca5f15a12418f28e69f5ad603a70294304d5706dcad600616cb8b75044990bb44870f8da796b91a9756ccc5aa6e70b174f1a5fe48a31ca30c9bf67a200918c7afaec820a03ca28d18d973e2179078d80e261fbb8c791661cd755fae78218586ff510f7ff34c628da786f98ca7709bbb6ab1c075c73a9b69c478e6e82758fe1c35d9ee165cb4b4bed550e65f507d96dd27de827550f61705372ca28107a495fe1b1996d8fb378f5735f7ed714c3fa34e23b5e4aa825ceb3c356a4eaf9f5c6aa75adc5008b53279a261fde04df9fee835c235904b28581cc1982a6e092b6da99e4af8076165d4f30edd649da9623e24c95d292d8089059fc5584182359ff16c39e2542abbe90b58b5d16e657e80edebca7f5ce11809d8a19cc33cbb082547d18c1f79b598c4c8462d3616b31a9bcd9cdfba8e30939101ba1760d8035da93e7e5efc488ca37ed7469c08a854e229165e226b72bcc4dbb7d2ea1b88db4a93783257d5aa82d95c916915ae5c4965c479122838f936deeac924ac49c37ac15ab4f5075ca02d8ea704895b2076a6c4b716052718d47e356d48327ee647dbe50e0d1ea5cfbc7e98c86367b9a417d37276cf52c98d9f43a04951246fc470afac0cf140370749a590155e456d5b9e32baac7e05e0679183e5806ea7a7b88c00017d620e7831245d9528272c5d30c1dfbd8226befeaa5c8b66e6d59ef9c6415fee2ea7737018593fec80d790d1a09b3911fe9d3fbc0f1faaa5b5619dbb4ef0e901747c4eef9d17a626e66e990f8cb4bd07afc30fd3f1c262f023a9aac2a9a9c12f85bf6499e2d1459048bae109c820766b75d406bb83b7936ed8fb941432dc0ef66f76cc76c7bb8b62718d39302c424d18137c948bec9e4e7e30f42e6b40904a1bfbad7da517157cfa66db319d49ac61b849c3f0933c4f99e65c097081617b86439b53408db86a0a2ab59fc88adecd42741ea359bf288676bc7231e31d9041d7b7fdb9beea68c66caa703f1f04f52bd31d3310fef60c5c6ab4aa11d936c4145c587c1e1d839edaf72b45a8b82028dc0a7bd3d62b734c060e87dcb295ce528434ee1864ec8ed77ee26fa142ae106d998f705722279bcdd098339058672448b6649b52fcac5bc46cb5de7b9ee9747e0c5ac370e5838acd836193efd61f54c7c7f62b867ed4ab9186f664e251bcbb1d1b4d2bd516037f6206744922489522b42713fb026f0f7c64b043eb05083e3da9e8d0e64d2e142c1a335e610b937966f3c55102d65626aedd13777da6aa75014fc11509246293a5d8ba3bf2461dbf4ae2e84fec607e79b45edc73cc6e3a9c5643b35fe362ce3223d539f67ac20b9bf4468acf34295cd6f1f7e737965c91b4b969c035831682660cc85c23f69c749eabe2c59e4adaae2b50f9481fce4371b7eb1966786b160b5f4b714ecbaf1f073746444d3f0a18571e7fd2e5e7867670f74c83859be99bbf7e9bd442551c771ee96a183a9aca740e5fb4fe9cb77a1bbb37ed9528434f4dfc8836db2fb4b259447f3488d4eb2fccd075a44b88038b5c928fb0504e0426819a480cebf552384351d012f0a14b507c00c9e42d7c3025e934d1bb57b07e7a36d5f003e7ec942b266ced6d921b737fa34eb73baa6b88937affcc2b8fc38bd905a3adf2da638c4918255744a16b241838a0cebd653174f3affe1cf38cf6b0ef4cccaf3ab506d1c5e1068d8b28a83485c04d4a563a362f10b7ebf804cf852333262c40e294a305b3eae1aac0921669b0e0370f94ca0661ce36bc6964c7912a77b8020d06cb608ae2305eb7e3b9ec47783788a10ed47e729cc677a33f43f72a9ba329837bc042428c637f3b6c28f601892862d2e2d7058e9db82db461418feeb60f505888377b8761112e49d320dc46695a00c5011800a5508039da475845bc5dff7a942f6ea2539d7aab2bf6bc28f11430dede00b38567070613a165fc6e4666023367e534702df28e789e5716cf37b6df7206ef853ac92defca9a1e72896669e5f43762ba761b1e845b2ad4aff77ac133f4da13a5ddb903659234f92d9e54122c6168874e4753d21b4c3deeb64de7180d1b20fbb217284f4d50d453407407fbc0611f9744357647de76fa504a0a52626ee1432857fea4b06c820bbb03d946828a3aa1cd59bff63747c0b6b35ce9461a2a760cd75dbd0e1a43e49df846368bd28c17eac658112bb5e94a2f17c7ba37d933819affbf1cddb5b9a5000044e18208700b71bdb867b8bb53fd7efbba3833c9af336b4251a5d965a5c8ed61c46f97c56165b182dd384fd1997c5e659df2764a1337c5b208b4557a891bfd4217045c605da8b5f0dcba4c4279c9255bcf31cc1d5f9b4e812218d8d23d84dbea8a818fafbe69dbec49fdcaa5936d64f750b56e78282ae47d097f1b173c38ac289c21c85c2d26dbe602bc745ac2fd1911defd423f1a267e0726afa0178cd6d920306894a08b12be0f437fba0fedb74fb34c3da4fc3d05eec55260ea078f985dd5599e4c9c8832460e2b00f6d952f8fb31da7cab8deb14f96cbc7efe1b205ffdf97af2a3864680cdc0e729256dea35297f83dc991d97e0aab8b1c6b43a37c8d5460c8fdaf062538eefa766688c3fcb8fa0b5f95ce26ab3a2497fad61227a2d12272087963095c5348e50174c5add71513f67c257acf441d02dd070c34f023b4e1039c30bc3ba7a093f6fa1a5ea02bf7747f16e489ff857299c504d138009d97b13d6357f903a5fb88730f95bc092f7f94cf2a6a42e0dc9a4c0463253dd6feddc676dba40ffed3ffe3796da5071404832c6e8bb33f9cec5b900500fccaa6b4928ef5e12e7a91471c24425ace009934f4d006b2798a648fddcd33629e6bcf1cba1ac92fe51f721b990e5db8b262ed4b8fa9f7998bbdfcb435a20fbbd04690e7944118d94ea8adb8fb2c5f528248b240ff8228e0d8699a21d68f38282ee1d93127ea9d1a0442cc301f3c4a5469dcd15fe895b779da9c3820824329a4ad4731395f0cb10af641ebf918b4a4dfc72a323afc2a94799e86105312ad316247524e25fee47130e178845d221402c5912c170e01671c855874aff44181b486f00c6ab145e43cfb6da3f7bb3096ee558197d4bfa4c48278ef0551b2b8c010415f0b34975a8dd3d99564766c7bc84ed7c879fcc66705015101ac178f86b66a519c722862e45a74d788c9f29a3ccebf2e1f9475a100d8824cd7401c6f6a0b46a91866c46c44d844e679c9c512ab7910e3022f581c8bbd3e5dae42ee2b0545cdce8ad26e44fefe31ef3e1b2e1594eb3a5c2a32ccda7f9586fe2db57dfc50f74c0f857b5c953ff3cfacc1cd6187e747c34f365cb97caa18522dc349587c89400d470df2f5c40045825044c9e4721624da1b63041ae1b8b1c0f2645604dd6834762ad0c58d0248f28656c82cc30182e948518716513cb2414d1b3a84a7e0488d75e378894861494d9e6f3920ffc4531fb2e183b5bd4163bf0b289a4321ddc58b07ee25501e745ab376ce0a0f2e168255aab7d83dd2f9d9926992f7f3c0dbd6125c8d34b85817c38e076b44f0a1a2ee254264a2d13d215334706a8719d32a4348cb5bb6675b1248d332dec661e1bb778e1bf59aaf21ed889fee5950d6be5fd40bb5eaafe08fbedccecd1f64bf0825eda414d229137dd2da607362ed4a16e5e74a8612f8153f7dbed54a51da4f7f3bef30b4a1090bf0b9ce4c8f26012cc163c24c088ed0f596d51b71bdb09370088262ab902cc2a42e539a3687057f5365f1824b41a27028b43cb6a08dbd6a4eacafacf3ea1c4cfcf1e150c0968b6072640689598ca8e37e79cd6cf82888aee4fde391442d32b9fa4c1737be80e1930f218f1ca065002f1418400c1786260aa5920d286e64a96917564f3a98f4970bb8e5a2ae9949a4a44c5840928c2bbea8e688cdb5870d27af15b7454844c96bda16863115a1b3a0b27a9e62450c48549c766952113e81dc92c2f2a3c5eaa85ffcef2e7119aac5497ed1d334fde93ea265dcaa6de9ca5a80c90f894c3ed6f4a471a5703a8ed1fc9aff451b93fc34dba0fa14b5334635d5b66bf2a7facddf2e91a8a1854e5164492c03e6e1d00c6601015355995c6859bd8848ee748af09793fdd70e3b9ec55ee4b80db79c105582657750e6250e57005b8dc4beed56ef5550d5ea735a8cd8fa6f0da016bdaf632056123d1319345518a785dae50f8a9e5dc38b67da4d1af275a816ea345390486c2cf6464a9021bd939ffadc7368f1e0a9d2d99197855022cb380d667c23402651e791b58cd9ea8df5fad4d5100ba55da1eeeec95f06ecc7e36a64d5933710d0899ec3407f7b2078c671ac56b374b1733b993d948e3fbc547f429abd79a5fabc1aa78a854d3ef24e19748215b05987c1bf67e5e547c3285b6163d0048f054c65ad502435909be4966df6c1fb3c87edb3c303c64a6e453cc3719d79e0c7d385bcb658f82fe3c64ee92632fda214f419b21fd0d144db3bf499642befcc626c27a0fa9d4ea4475360e8b58ae85856840936715d211f224007a51d2516f29539cb7a1f4687db265d19c98008e12a81e723a3074208eb085c607e60f9f8ba809ae8f7c50f35d92bfe4b1cf36d19378249483929bda4a4c83cb9b5b6cefdc8a2cbb0f4312a409ac5f38100e373240fae943b98d5136de3e2e09cc557c81294f6b2e783c12f7fe516cad1578a644541ede438b2c8df6f6c34cfa464382208c5529dbe904a50d5ea5f2680a9676534912c015fde2c5ee74829edee47e2963a9813d9bbd21de5ce02af5bdca3018151c946fa946c9ead48c9e115ecd46982ffd20e96388c44bfb81f5182f8e857e7bdbdecdd361e6414062c4b552bbe5a0d19aafd38836cfcb84674ad36dddfd696e2de8d55369de592cd67d2141a904070d8929305ba6d6b2469a0949fd12fb6bc7e0b0ec4b0756c68379da6958d4624d9c335a9ff45199ee973bcd8594b3ffd0ac81c86931f3a012224e8984df316ff6b5fb81217e4273095cd8d997d8f48aa365c2a17cb9bb6f1835dc5223783efa39697bc45a35088eb2296baf15ae55ec1e960fcae50c909157e30f27fc782d519221897ab1b6666841f6dc838dc73f94a268a0b41ea2cc1519f96ea342e5ce0b007a45c13215ac6d6d8a02acbde884646c146905f36e291a0f5c8735e093b189e1819796dd139e6ee89aca5b04e9d8a014d8bad5a214ca5f5c71f8ad729e6f7fe226a01f153bac76689e7597fc476d2b8ba56a280f6ddece77598fea7327380b586f619b4724a279f72dfa1a993a5aee7ac98caff1d3b7ea77927999b17197289675b438067af823eb10019c5716b57a2262b712b9147d41950c92c3cd9c55f3d646b2a80336d31c53565376bafec16c4122bdca7828aee054a24f08da33247a812050215697dfce619820e75064cbb24a238db7f7b89743408b0dc823566dbed7d269a583f7600e569cf6c48b1599a76479ca748d91b57147419fdbd6968f5782082a7d0df9e082313dfe91aa37ce2135fc146b4a3537e56fab067fda2f067f0d1fe4c578cfb341b081abd772d90266968e44e4738e90ca4d17ae61c10e3f7581064a0a345371514946ed8b89d63e0f10b24a9bb262a511acaeeb5a2c43fc80daeba0479cb69faaae4913da98e848a091a66267fa8ee4e9ea63786cc2c6e9d8b44fd4b4aeb8d2cf3aa3c9653a72294d9e7205bd3ec426e2ddd4c9f95eba57b99221d1ff541165bdec13428575454c7f677f9df108730916701c1f6b62f5ebf8b58557603674767b09536d34e826798d3ab614c66b88d2ab9e7d998506898bae0707b3c7e8ebaa901e8de9c96c5d718e5aae7382a9c4931d89ca6f435b090fdf362cdee4772732638ef6709e3552bde18d24f9d5d8c4ca1aab419831288438d7dc663016a70813b39661dd951a64f8215dce276d5019a75cc7a4bdbb4021d6d576441deaa8aadd33667014f46388f7a4a3f3c7842633b44d38d85f9c5824dc25a579451f38b2c2f769ae4c2feb6282946a5628398430772923987e90138160c3a0517fe89177e419ecd0b919ea6069c9e64d3826c443650ef873aa5898304c4996759a6e105bb9c96cfc79395ccb6317764c110a99987dc9ed6fa9a70f266210dd814939e2ea71aa34e307a93febedfc717c294f07a4d3cad163c913d36a92bcec3aa47d9404ec3ec30b4c796b56fe2dd35af676dae5c51dc1d5355d8f00c050a6115250f002a42de4e375fd669358f9d75d4aab04682742400c8e52001647c40f1b1da0fc40bde901ca75f95c53c48b0d0a54e433a3998566f9558f9d4850246592dd0a94c7d839f857bd7047027006586e691fd39774b2874fc49db84b4324a6e939385497bf0e3419780738dd329e82b47eb9ab99326a697c8dcf19436c5e2378bb2759038d3139d577f3360855a8cb71c46f017f1e1cb6a5b2df44272e8497601787ccb3582c5a1833f63e409dbd7dddc6f62b6a152f87b3e7e89807bfefd09c457564e444fb1f993280cd92b14423cdfbcb1653597ece8539805ee01d80f3dea116ea862c96177a689c2fbc1ac05a92694ef00cf60a210c4076274b76d77aababe70f23b301015c44618d5cafc1e74a06ca602b5eeb3e7e4e32311f155143f172e83077640f349f9df3231479bf89b9f97556aa5527941813d4550ce64bc94343570829c3038ff49378c173872443b945705375cceb87308948fdae95be2191c8530a09220f3b66baf4fd626e20a2e875ed95c83a0ff783097c3ce68e4bfd75e49514e520865490a3b4bdbd37ef10da60fcc21dc19ddf37d411085552f2b89ef8f6663b790e333cae6fe683516406422d0f469cda01601a5e0e0fbfed21a084fa0163be9999143594c52515e94812a27650f9f9643041ed3ae965aa5e90ebb6890f811395020c60d527c40b24206728e4824a96358ffa06c8656e94c06a7c349080672b842fb8e62851b9584e95a6a957a53cc2474e3a54ac4e2e7942c09122919539152cd3d066e50725952c7482f305fac56cfd864b6adbf847ba5df8131274d01dfb77681ea54b0f29162ddf25218ae89f58d6c5c6d030c1ac021b989ad0547c18bfb2f52b00b736ef4b3736f853428a914d49e64713182b5bcce58132c453830915c348a80462cdcb84b53be23d6b17e9c4e15548d682a8a181f235f79c0a666bb2b06733073b6c020d1bb7030cc3e526df15aa940257dbd849d86315c054ed4dd50b8d999721fd50986a569ba2f8f3a71f49517df57e16a90b1db314e643a6b4cc0eeb26c62b3f2dc34d1d293e388c75441daa55996deabe0a3108a839629945460dd44c0b0eb6c53475ee5a245f33ea9942253de8cb806201b2b23cb9e4629c93562375475283bed0d52d10b96b713f47adccd82185cd9c1921f032655f9c2c760b17c1f5c7e35a164574599a1b5b21e8765b850ed2610975384db48d4def38e467f2e09ae0b644f9daa8706c92d951135ba372040192896c34280f525ec0492783b4efd53eb3d0cfb92db46fdad93b323c99f2d85eb1b7e738b206225387edda8b90c6c6e6b0de4ab006e3de9a5ab15f4c9c3d5b01d2e4d97596533b33b8e2900e10504c69276a7d01253bdff3960a72be29939630fdbedffe2b7ff54be6d40fd9522728ae6acc00520f138f9e7fc177df799e751075110906d0649f3470d2a843f4239b10413fb87e1d7a477175e5b0e61cbbec1dbc22e117a6d21a53ced3518a7dc600fa04ed7639177979a31f9475f7a1f3389dc246bdab77fae98f2e3147881d09a7f0e103b7d47abeb86acc1f395002899ace4b9a5f649c7dc15b112810955244892fb011598806938acac37b68040c8146f640e29ebcd339eb0dbdc502d24f979fce4984f5a8e0c5f39572361a808fcf39b86ada5dc515eb4a9b5063b3cb8e18ff258a107838922703a422b47d8384943d0198db7d13c8f83d4af7f4e0a40bd649252f6dabd1aeea072b7d5584410a15f7c6a2ed20b175db6dd2edf9e9d68fbc6c7c4fd465e72879997490337dafd2c80b62e2a5a0cb1af1b79cfd6ad7b9f013d6ecdd952f878db9443237298df48221d2b66c457b2f4b39ea7214aca169002e9fb5973c2c3ab89f14ba7a04e963759ada9845b9ec094f291d0b739a1a3a4500448ac7a2d9bc278655ac234193360734eccaa97403952184000e5a621ab042e1f351874dfb8124ccad2046d3c591bbf8865fcd72624a649ff580ccd159b50e66e663b8dea996837dc63309539d822c7feefbf711ad1c255dfab34f2540a8541a961c6a3d740fd3d0eb91e76efa8d249f0ea814d259dfb49a0678a109da84439d8ecabd443d614a8f3073b600c05565a0cee56b6ece3aa50b6b817be9dc5b062340140c7ebe229f12ca63a1f09e20a68387170bcf8cd4236535b61b3c67f66449d178eea025fa71e0a5ae3b8ca5273c628304e8d3464979c581805fe14b6ad3753abfa6563ab741c355b639fa1d0697bd4ceb4e8bb661a2dce10582ea6c5bbff124244cded093bbd57f51e42381419aa9e64a034b0bc78aab9e83e4c0d15ef7c1f80f947e767aefcfe6c0f8e56be1a3974eb11a64b1a19430c913c046288a46f2abed553532feeb3c809f176700d973857ad300b2dbe2ada7245f7a9800cd682cd3ca7aae38bb73a8b6b6b8f666225addec2203e530646aabf3aba4ed5373856ecb7ecc0b44a73cbdc8a80213de7862c2873b7388b0796e1dd746ab0c73decff2bc6320ea157995cbe240a05b85a2978b1eb9ac0f14585a1ebe02e469541a5919a3e506bff400e014adf30a2d51fe44c84638aec9dbf0ee9c59290d747279009962a8a07aed8f8861d61a55eb92c3f46ee6e46b93f3ca3fb200db8670e3a1a6eb89f8fbac58f2a7e13fced422b631662a70cab54e3fb14f17399e26ef07341696d09bd39e311fbcdef66a709da249b00fb939d7bb34160ff6008fc00fd4a484f3481d4b965757b5a1e5898c4c71462866386bd4e50f5d1d50a658209a36fa18a561b0574ec558e7a0a62fb7d1559da6b786fa57238c9ec5754d58202ccdd3b1f03d9b48cad2d1db3ad9de7b472be67e2a6a020ad355d015e8578d69820b44e6c46c65b41e9778f629d4d0639e6e76fac6068a440765711623be4ec366224b8675245baca92af3e0ba9fa4bfbd516c128f4810aa51f31aefcaf154b32e828836296f02549062c292a872e7daa7df6ebd25bd9dfde077bbb1c722378288283aa3902a938f11728ca3080be9dca2c14c8af4554af3961718ecac4701705d37c5951ca4473a16e493bc2969f857e4e5f010611ac7545222c13a1b23ea6e624a533d4f1eb01e17d3ad0559ddc0f8c581377dbe1e78ce904aecdd3873e1a2ae5870e779c97db5be4f06091af9a4992c9d43faa2423493060bbdb30902cc0ece26082cf9943db9bc923aaf72419ba32ac3a8db2dc1cbb66325ac966c361ee6cd2ae4545398f2b75c85787a8563db4d5ece7fea86f466ef58d77c955e205d8a1da6aa518487d73f1526abf2f6a2b27bd3da47b1b78e03fe7b5a472cc77cbbe53ca226b442013ea003ba9e404633d2bd262e72834a2fd3dafb23282b11050e539f5c57b8ae82b9cd7568ac56cc66969219be4ae05614e629edfeb442317d0d99e0ae19bd281d4ca32b1fc4f2ba4bdfde7ef6820f00a83c5803e3bc3a42a51969a01181e6a29bf898f01b6d2e0842d9cd05c9f7778c4866db7a322ad3ce801cbde6bf085349e707ca736d2661cc992f0338953ef9640e8b9a896acb474d5b14d66e977205efbe61a4fd7c5ee7d7a8ada83a31c3b56bd84d48bfd5f02caab66edb81b1c1ae6238e6827d0b5dd5af0eb9fbb537d5736443fcf26072313f3d65de27f657594e965350e849613ba0052e14e6d2a935746dcc3053040259b992637f31d791902c8b91719cee7667166938f96f0e83c5913269f54ee1979bba7e7f9de7943d932c839291b6be62911108d32fddc54a3502752081d36bb883b060bfa16d1bd4705a6367d32fcfe7fb7d947c2cc61488b1b0ca9ab3c31bf781fc36b659b1a0b7d737a13b09a1a736c42dcc53e842cb4ca435d40def30685ab2e54be77bc0392aef4421494c11c74a35b6b3a0afa8c481bad05e4585e841e9bc25974db151b79bbdae7b1789254a57a185cb25dd10c2e83578cd2bcc3bdd7e623905f6888accf85a27af0dd7ecc849dd0f0b227d585fd4e77428840914e2a44ddc9869f819b7b6c36946ec8c79b44fc7618ef1b3c17ed947e6261bdd151aa8cd186170a901c0d2c8bdc241214abd0c2ef482767384414b9ee8071a03cf569061208abdc4563c2bca0aa04b2c0fbb04bbb39d1f0f9af261b2da902d284ae0999f08d60050fa0dc9ede8022f230e8c1f6b6a5ebaad1935c0a8a23abe3141edec012eb2377c150a0d31722d91374aee092b188454c3f9d1f0909638b00c5559a0b6383fa523b72fc984b4d4ba1224b256c9844da91668ca15df8cd3f4740dea5512e9505ff622c0eb2ef700b0336d4bdf8edae510178163b56718b0875d4fb141c22f33841d0eb523648ee6d30c2ad30132d90df25fb3a4fb85e26f55f264bc6b5b98665a5e35a4aa76baf86b1003ff3859996905c047c16432d5916bf487c3d622b746880e00032ebe3c2a3932242a65cc17a713e77809f56af2aef6bd3f10484e0ef8c1cd8944ef2df1d35d4ad4beb7d0ea9551d1c0ee30a0e953503f4895744b8473914cd12ed93a22242f28931dd71402297b25e85b81fd93ce9c217ef31dba52901ea71a70871c3985da0484ce12ae5153873439de29f9a9ed8796f20b2fc967060ed354d70cd33e8c5ed572989ea087b61bcc3a9208f7eb0b5e620c9fb0ac36ac4bbc11679fbc981e27c29b16d590f9acfec3e1db82269935f93dde437d53ca98bd4dc77f581269650f41eeaf91f97ad29e0e1526e5ad99086d6f88e490070a0de0d8e3fdbdf0d5e5713d4a275554283889a325e7e35fb75a8ecabc6c4c6234efa38139f1598d83b166ca2e41cd923907a461ec5798688171127265218f1fd1db06cff391444564c952b53be09544c60d2533b4456a6dd69264744a2a8d3f63b1bfba48ceb75bd4d2ada3a83d81ae960a3e6c97d14c37d6dbe967cfb036cdec0914743c83e2e2d33a9dea42c55adf982c9745291432695c8c3194da49b60a606dceab4dc67eeacd7a0810a2c44a2cc00bbb62448700f5ba2083c315887283b11b52c2ac4820e50d01a1c6895269a71c89a658adca1e26490f835808b8ca9db7e80cd3705796b4d3281c59230442d8af746a4038cc777d7cc1077c32810b63efe937317399c33a3d6f42b85fec569f508dc977b935ba46fa86588111a2b21e4d0585e9d3c3e1cc2dbad15f1cdfa83eb790a2caae370df8b3a023a7c8cc47052b6203899b84af614233f19d9bc814c7c951fd833153077abb0ca885a86ba3248921ca867e6894ef13f80c4bbad512e4e4738ab5d417a4c2752fbc4a0b4e2b67702d3be4397674d68095059fb297db0aa9c84a1834faaab23e7c7de2006dee8dcdf6084557aa7f714a14cb7b1a0378f15bde0c4363e4088764b44016043ec04bf7437ef4d27ededc8e3b90af7ee8f515db177e835fe34afb9036a2de2d07f6958cbbcf0be2bc591cb08e7555f4eda352421ea15fc8d320de1977c45242b4b394cf4c588934a79a181c36ca298c9d0beb9aa1d2486c3c240c73db39307df596a944a2ad1e8878c5e9bdd3ad171b6d96b0551df8c8a82b9908f14cb1b2b6d72b4ffb9fdcf79418063d833e7cb40b54b10e25852cea27d630449348228b5b6072c6c740813079b2a9b815949deb354fb68a01b7713753aef2b6a231a875003a94ebbd44735531e78fc6b052a56981d546cb3c563ae9d4d7c01ef1e858c645370b33d08a9ed206b1bb48c7f33201a0bbd24b710f1d8f1d27b2007a4d3b33d645b88462571b35b64d0f5b368e0d3dc3e6eed74a4c84eb463d97496741b921bccaded99d2807ee4101491762155dd4b9c488df6f462c2e8c7a4ba13ec4318ac19b3585e3c834dc37be3618b83d085df2db9a4f097087bb0b97a41b071ccf442ad0d802d0828dfd96b123599888cb656cb2193da46004005caebcdf1fdace3905aec0eefb2d73ee7bf044ec789b27eee670eaa24c7030aaedc6720dda91637dbb0f004b9335b168ea3bb365ac5433f8dcaf8054d399b0923fcbe7991dcd9ae1442147371e3d0937ddf2d9d3fc36de8a99ccab24f3a9493e37f3034e49af3e330ded4ea1bb4c0d7f5cab17cb2542a6cf2e66d8ba1d0c950d8a2dc1ca1a6352621ebb0241022be8dcafb604f167d1c3283afaba78c29251a7a7722210fce9c0343cb68c3ede6bd6c1573f05581f3b8c891fcdb326e8d78caaba6acc202b71d88ddb898a5eebb7f50af3e59bbd5d1cec12c09d12452cb7262d830d23ef14789bffb3b6a09be819661c849d902888994547cfb79e312dbd1b3ffef6f1ea7a0f606a3545bd056c1414d9c277ba302d1d96f3077494ef6029e69da789aac811a7636fafbdb242928d8d7ead40bfe3a7d961ed3357beae432ef6e60004c6c94cfc472b2d294568d2ed73322b43ad4e2eb501cae8242b300377de07df51419120ab608ec2e6c99e788105d6a389f581b2e78fe89118e68d948bd1fe2053729efadfa4278e04a184073b08311d8d40d3af117846120010ee2e38bb1c2c60e20fe1e784ed4d38a8b23046da04c6f15cf88d98812f2f94446139cddaf8a5bb6daeefc45fb3f2c1b130347bf6799abe0fd26838244706892092ea5c1b96845559c9a961a7ea61e687ad99f7bf0b9e9bec48c64d1724ed3e8be9a216b56921aa807c7b10b153088e172c4d95962d1ea373aae15d9826802005f2abf1ea5f55306fccbbddf541e56e57b45959ba219b9fe2ffa2ee986130ea21198cdaef27dd56dc02e0d2665d6e04d6a516cfb931b6f5fdca0286d7e9d401d8bb44e80a7e143139249df32852a0ada62e59a1dc153b4e797a9418166af4055a20162d36aa677b6c009cb06581e84ff839b0c6a6f5dec68d1bed05c341a6e3a4c5309003c68cd94a9649b7874f7e26c30319e87b91cdc88d8cc4d1ab27608cdb8db1801d7470e8b1f668360284975a193fd09829efd229dc6f8631f566197227a56628a73a26873777de1b62da93f6dd5eff2115732ae848000f7ccaa4258fc8b3dc1e7829b503dad60a9f883f9c0b86da18276405b7c7233576d72f9058782cd180391926878c24fa3e1ede88f68eaafe9bfd0427b9032305f3b14c906f7077fc5c4941583000b2bf3ee0a49296d26402da07d2073995ec6930a7c9039794ed4899b7bf9901362e93d315b71bc90703a102dff931619404edaae5cbe64b8e712d68d59dd2e735cab304828d8ca23797839e484ff56874b672010860381931c3ab9d91697798f511972fe893c85f3207b35cb81fbb024a481b43c59069d25fcda0cc13422f6d29a98275b62ebabf8209551d9d293cad216ef561efde3752ac4bc1315e3add96ceb2efcb69fb50f7db5fe489ea233006badde9fcc213301fc853834eaf1f52693725183c550ce656af1be40b13f85a3ca15fe0706ac846453e11a69340033ae6871e00bd1a3d0e12847ecf5af46d471d674945a73241860471c8fa43dd9e8f5e5345ebae09771005769596d013ae4b4ba58b117f9c46b8a00528e2c006e8e3b582236e518efa232bccf477180dae1ce507cc8aaf1ad7092d0c7781caa56c62ef65f6e2a9e44422cf18fb025e2abbc3de072020bbbd929fad0ff19e8d5b6239edf394a435f2c6af6661c026010a1a79c80a7c69f4c1a305b54c6e9875e7385fa714155ad7aaa34a1743fec0c60a81605bab0e98511c07576844f0ae7d28de29abbb38c3c2a30203d1af07724d46a67b94aba4d56b57052904120cdd862388de70bae8a56a99ca7b3ed4c70299356e5eb83b0891f5ee197cf04c6db351835a6d61c27305c489a5474fd60ac39df45f480767977c98d305e220fb00ed11309ef786c7220fd897e518176f9c5465318e5629ac7f7da9b7bd1c072ea35e172e6a56b03aed51b46b4159dccfe1d35639e225cd1334c5f07509a1788f91c90dd9fe7cf2fa0af6c9d67d55d2cdfe122ead3911d791254b6cebf3c108fa1208e7c2f81ede7e49856d8f9b42315856b4562d59d2c11221da76e16c3b0c8e180716b00b61a7cd048914a4ba5d0e287f416afc58c3a59bd8d9a617ef672b77862168c28f764498f14a49d79dc7d2e2bc4017e853aca631f396cd8b63620673d10b8e578c0466c04329fae78a07d0acc47a0171f026263078fe0a5f647ed73d04e017bd042b4daa10ba50ae7e7d18c33b146bfde1c48bba4f38c16ab267d8d2f15b7eb1bd1582212ed8d0306a482b154776e0834243b570e267f65d3705b99712fe98932bac2516efc788842894b650db38a2a2d16bf321221ad58a99f81643e8db2db7cff32be471d18bb80be76826f897fae6960e94faaa960dbeaf63b320a0aae03ae5602919f67d026ac7efca1251027aebd1371bd708aacfc5b230432956075c5c1f84c8b3134bd9f37732119f7643ec7a7d13d6e552fe0c852f43cad71e2f5976a8c47b975bcf42e513ba05249a37d8021fdc1d68d9fbe3f08470216aaf93a79b9063f2f5180ad6a3c370999653289e57da8bfa78f18171d3979a7dbb605b6b54251aaba180c92dacd7581f57094c33bf46cc851d4fc5172a27c250677314763f92885f4316d429b3ecbaea7f778483fe4871588d330cf0165155c9ea8256c52fd41d7201514f64392bc132aac250cacb241c4ecf51fc9c9cfa3d64e552d4475797198a6777cad13637b4e9d32dd8199599e6ab63847d4d6e13b093f9b91430d946a4fe9de77a916998a76a5b5f112ea019aaefcd709651440eb74114d7595d4aeb5cf0a54d6302533b9a36d6510e72952b2370bb1aad7fd158588528a741eebc74f64400ed0044cddf0537e9251e2293e572701fd77d55696f0bedda2a840e31eea290fb32015df16954b0f6e4b6c436ae3b19b5e3c31f2615e5fbfcdae78bcfb4127a195bb88c0d552a136022182f9c8908fb0ef7df78d3edcaf4e537fa70f7de0aaa24a211ff6a547d3930c5f101e2fa33247ba7ae8d92c3b7dc5ab10371a714b53e9897818f0aa8f2812c4db59e4eadbb6c1a80bc7c5f1d585844cd88b00b938d1d6ce3e6abf1233c45c4f1fa1468777a5f5638a31e8a47cd9d210cb740b5ea92868d41c17aa8675fe6ac3dc3cce82d84e8d38e67bdb4155f07ffe571315246c0c9fcf0cb5aa61016a20970d985647d26f881203818f2c751a9a915a89493e1055af9cec84f51490b8b114a9b54e86b384a15c16acbdbcbfca6825811cddeac08718ebb682594b3a788df7cd6a9d8aaaa2d178bdb230aa69b82d61d87f8cb643b5ee15010249f6f8778b5c40492378539cd9a732898f43f3cd70d6199f1d5247b7649db94cd9c0842cdf95dc7e77cc39fc55df836d66cd3f49ddb5d7593650751d48da5c4b518f48919b101746256a9998b3a9b8d86f4bf439951d451e79ee233349b1be8b748a732274c256e4274514f3ee64a9742e84b8a2390fbaebf9850249f93a44488fc1d04e162c5e9405ab253376a6e9878c2e07623c9075878e378f23c0c780f254f1029947f6d8f4cd5e95b566519d3809198666f755a12aa190bae3689b563a042930abb89626cc4af4d61c3691d2ad9316f7bdadbb1a69c745820571298600a18e93fe07af2a1ec3df363259f8ce1d33789c820c7318fb63054a5ad02f5edc094d44a163428ad8a2605c2f257dfc4d4750095f5164e1db23e6c7f0ec65a82382dbaccd50d9b21cfc596fc887d3bc850a646725b384074356b218812de30e4a19d97aa008342f6318b373539268197df0c550194ce56e33aeacdcd4753fc122b2152959e6c2c31f9e11994143c7f4eeccf503792dc21bc65bab0c40cc588efff083a031e92ff30e0ccad56c1abe04367803a4d537cc1acbcb49207aa9fe5ad78b1ef3cf4800d731f5775d54f3e36f88d7dcff6e2e52ff2c65ac132f385a9dca80532aa0e7a1632acdf2b50facc79422a9224869cacf4684f3075271ec6a14dbc5f71b62ce344319ab97f6e389e3f8664b72b505e5a85f22c35fd79b1c6b84bc7f5d60303269ed12beae494c60ef0dfe7be79dc4cdcb13fef9d56f9ca764d6967f796d821e89c6f59138386b8c456bd9d2d6a223dafc65604cac9ffbff2ee0292c465522f1d7b497f21414c2015e0c1b9bcfb3db94c2f5d87182bf163dfae4ba6faeac31e7149040f7ea317666bf1f3c97018b69df876b300adbcd8e54af8203d751e852b603d18ef60250f6f83551280578f62ab72b1bad09dceb3b0d80ba801a9a1811f058cf3b357b662a7fee7db860a43d38013c1dd045b6517613cc143cf94d411f476c32724d315a00e2da1ce48a1f90b7ecc56c66c3c7c4831f956d7b72d1242e24d51acacc15df9849eff4ebf3a9cea63b5d14c05f244ad589a3979667fae0b0284d9a57847287965bd39030d586f5431d9a4ec43cde987154ae29927e0f7900416ba4164fabf475df6fc9bf20fd1d1fd5d5a247c404c97ccd00d6a7616b48fbb365790cd19b678b7d50ea682e43a6ec21fcf6d8303bf94373950a84d0f777aa1209075290e174456f4f1f29ae32033625f5c6cb00d3549ea4f52fd85a88b19fa9817e237fcd1687c4592ddae3fdf0957096691a1aa75e6da09d9c3097bd08d285e4638cdf16df28dd19fa3038e7d11f4e9e6535c1be27ff2dd1711f53dfc67c28a0dd6f6d94ab770d08882a2e505da7d2e5a1f9afcd05ba5b12f8ed697d7be6b7ac8df76fb87f0c5d9318c497a0ab0c5f7ebc41dbb339fb978fa3f0ce0d447e881f71e16331aa016e44da08da294b020048caa0bbe3257c154826619092ae36cd31ec95aa03698577275e0b263871ad9097fc25bb45f7c2a6eb1c77151c55cc6b92cb76e95cf8b35a3a90bb5f4053e7536d265a8a21197cc62055e75fe154e0c446e5a3d59a1f5524e386a45c8912a5ac242c43b1f12344504fd6d789d0b10049ea9cb0d03057d4cae8990b315a2b14906bcab83e7f092f1b996044c290758b56e5749621694d40ea71b4af0064fe054fa864b35fabfebbd4057bf010c30529cc01ef2f74c930c09d40eaa65d66cdf37d1af7c9a849fcf3c139a72180c83b4e77a977b1a90978de27226eac750effd223eae5ace2996339741a0d86e241ba4b9daa9b3fe301a064f3ef9257c9cc1b2bdd021916e10f6532f332a64a8ee71b4ae69fce92e7f214f174374331ab5b0ebe819a97f5ccc006d7ec789bf8a78b54823bc2e87adeba504c56eef0d28c1a06bb95351aae18b66c7bc2c45240a817e949b47669f2cbb27bf22388c060142796c98d213584fb9f7b9889e97239b956a1f4978618efd7581a58eb0c7d4901f03b4d911c99138d3defe6c46e65210b29741352b6eb3650629933211c3dbc3b65f326b205087612184a5dcad9b7ccf5305f4c8fb767e9b3065a278b3fe81ed63d688548f01e1066ec0c5ee076f52e3186b82c142bf50e73b5ee7a064ba9acd14ea91cb7eab1edb3bae5668ea9a0beb767fa234e30ace22aa7f22b03932febe40b6d5e7d02d46f08c934b19dcb6698e8022269b0fc9a2ece43f188a3069c697937b4745d7b9cf6f2e82e5728718c31dd5d34b63f82ec4e7915921fa7829c9fe06fd034bcc63cd5cc5c23195364bc1c420b9d760c20f1df875b5e71aa123e9e341a7bd44ec70f7ea820a40cb7d7dafb614b2af7fbd97a2933c9a84994cbd103fe60b69a8900e6df1f0daefc983dbded29c559df033bb3bb637467b338500031bdb54929949659fc326e5e7d882a807f2ea27477ef07018b3593e81cf23e4cf60073c10509d59bede311c8a16834f730f1f6b86115cb317b3d9f32d5ca7a562521c57cb2bf63352df5d09d2167b404f3c42609defcc9a367f27c01b94c79e68fa7603b3f9c8bd32701dc3acdcc246ef994c2f7495ef343f53a177cc0bbb34bc38596f03d2f62c1b6789f3c75594535fe027ad912d2bc54e639d8da41e7134ce2e5d89f66798d0a55ae63131c42f4f39b67bd38e216b1ac231b0f5c2c28cdb55c013a39419025523585938ee82d5c8b6237cd331c5ba55b977d7432e17c663aa214b553f2f085e750098b2143822d2dd03753ee121d3d2ecd7a140c3028decac2e09bcca03946c2aee711dfa56ab02a3b30421c2228949b352920c337127ab439e58f4570a91813d204d342bc5bec8ea191752c51433a5055a5ce807aed73074cd0d15ceb751c0f5ca20daa03054ca0012981114750de99e37caa5dfb4a98f3aea6be217a17c5b846eb1c6650a2b77df3215373ae8e6f278bd134becbbc5eb1ea7c4f20fb184803dbc03063c0d054de57a466261ad7148cc03f7c55733336ada1b631fabb68f6d85484e5e25aa2df6bb4b08f51e02af652679016b8e31e8e81407385edb6861f3b1cfa455944c16b3bd0f10eae6ec82f8f2ade4f13da5a98ee4e197baacc8bacde708d7ef430971e996eddbf8616dcb61d11d445853187f1700a695c7c3b1e4fed294cb2d80473fd4d41f3be2eee836413ad9a1366d869ad2a74fd73107b4b75606e2c81d5190a06f5a9faa1094df0751d5df2f9bd2d0147347c02f5338ab399462c2d95fa16119af2b360614b20be3c408ed065be0afd2aa5e635aaaf242efd2d24e9ee0ccc83736e0882c15ad5cdbbbf3a768ead9120029e2e4f2375a0bd44c7c7452d2f76efed5ea53ee5d20f0592677e7cae1ffae0a0481a4cacd1829f31abd031354494db95f183bdec91b7786d8621b1b71d75af23cbeb5f7daead881f7c77f65c68b83cbdc678ecb4f72e5b0d1e839059a7e18bb126d8703fcee1b516c2ef622f3e6c0695d0a35a56faf596a208474d976ee49116f160877a3360cc3d8aa6c3720b1e2da528622baa16036c55c57cf854e1b1fc6e00986bde0e04564e5d7f7302b7bbc70507d3231d0f1e136d352b51f2ec8facda5fac9d39522dfe2d4509849275a4a270fafd3cd879f0692b6f3db4132eaadd9662d0cc7c222f6e265ce63278225a61283c1f15ca4d9b24e859e526fc16f3c7f17b65dd065cd68c90751ea56557b7530dfe710c27f8daf2c0b6d4dcb9c987d32b9b869e3fc33ee1791256ed6885bf01ed00f14d0740251a3a372e37bc58e6259c2c41cb0a1cc85aa6f437638e8ebc3a2c6352ea3658314a2d5f87a211e9e071c542d2ee3023da5837440bed7645aa9e0caceed1efc92ea6765e33310bdd83a9b14f355220b52a5e2e7f171c5947c37460c3c5f655aa9461029346b18ab8692911bb3b6515b327cc0d67311a00da3c5717f993ac010914d605f5d348f4cd887589b13a6b517a772a4d482045f2689147814643689e570b2a86486464a318dc74c6eb0dac5e92dfec033ebc8c81ac13f2dff7175da058bf2b534fa5de1ef3a7715f6f933a1ace3315aafb841d45337e80453b1ce81c49828e29770d4f52bb98c84eeaa22a080a62e4475f4766e0d24e4508eb8c4ef7272cda65037a1d2444fc5fb6bf51b32d51e6fdf1a29eff1795e8d75db6bed59422803b7dc27af15bf31518ddba97f6c929a66d211b693235ffcebf8cb63cfc067cb19e3fa488274772b678eafbc31de658823ba00210af331da778edcd83c4f3fdb3c83b4cdeb97205dfbaa160687295fb840b2096f7ffc8ace0fcbfb771292e6a4cd03d5fb0c1bde868b4df08faff7d6812558b2c4fcf98ae7cd2e8a8bd2ea0d066069adbba4575a6b5d04ff9db8f7cd0ebd6da3912a4e9f8c043232a28db38e1b2c930b4270b59cc81f43fcf238bad43a2661fec37505d3d21b7a1d5fc60e7c452620d78d4ecd78cb02ad4be6c6e614c7dcf46db48fd078476278e7b16cbf4773df6808ad5d043b1d1a2c083dc6416bfcf0139b1bdb5ea74e27f6df34e02f45613c5aa4a23b1354e3622454b4513264f3e9361c43f12d06e06d82bfbc3cb2e786db2ce62c0db35bb50c5d75e21aa81c79da349716707118c310978a0a6d8eea9ada1e51e041a5b570910f204217166af883d3eed176a9c8aa42bca535282e05096188c0b11a47a2bc04be933c8bb4354c3abb41bfa39e1a98adaeb384d884869837e8d70ff53427865fa9f4b5eee550c775353e473c270c8372df59c44a12c9164da01a98d7b434579ff7ee4718a0e99334b3f3cfe8a198b4f8dee70b26a9148536c79d65027c2c0faf9fef0b3f15e54bd9165d7986da30fb470aabeb7d114e9cbabb527d902a41598ba522168c36caecc20d9d9a760b856147fb8423bc6f59bb235edf761a8e76fde61cbfe1d062328df940e472581615e757a9eb86bd25d85bc2b4de3db8b881c6e5816de4a6edfde476667e8f0fbd7c35d7956d2980fa38c7cc6a87c2d24040f1845ddd165c33be2e90b54a428c66d16adc465d47ca1e3718c2b8009db3c408f6c88ce72d5c7869fad7d9200b8ab714d5ffbffe9dc4b6b1cce9a876271f027e832f95f52d08fa053ab992b920585102e2d8094e29b8ac3130afdc0cc3bc0f7390ffa5afd6c7163383db19d6b1486315458364f9369fc1b801cf683737664e2997e672f410ad04a2e03f1b8d92e470099cb6dc4a6d709fe6edd2fdcc3c3722228ffce8db27466725b466a0f74af137dd118d5385adf192588f332502d214c5f9981616b7d0e5a4ad99fbc0f38bb985469d4017e76d191aa8ecb2cc2970406dfb81d902bb732065cbc47f4aed3ce72504e2254d7d0e0662560362ff5def6f481bde4ceb3ba3854978dfa80f55389ee94709b4cbddaea2b57230ea79dcf318cc3581be15a91ec897d70f0fd08d3b98e9db873f965f3b8c8fd43ea4281f7938f1e972c97ba894a6b4e503e3315adf5e7b0aab83cc69436fab329fb347633379037cc5d7a9bbbe899007a5f60e6698a10485b49b61af486ca81c04cc9de3ee2576d4aa4164698c0769e3b727423a8396b3667b43b4dfc7a637d8605c8f690c34fb9de7a78d8485d21e72b3b36bc9fd24b3c1538347341e11b9cb4172794d56ac48d4caeccfc09212032a7056909afe2f948a2d3315b04fdd4a9989f4f9ac04ae41c887cb58409a37d9697a7cdc0fddd09bf29180aa31151e7301770d0dd1ec0aadc38a6fb5b901a4285d1e3419c12b59313ba19dca34666bd09aebe5f42ce0ec757183a26cf42c705feeb2291f4343a2f95e7e22c9f3a051e528ccbbe3617ccafaab59f7d3b6604ca884865bfc7c5a9988a15d953a625f99e4b2b943450447f7284d5164856b5ad568baece23beee87671e9b73b3ad070a6e91ae51e26770dae126fcc8c74267428d4723129a1bd977acaded7c25e911c458583457afb68930a429cf243b8b283a4935f192b884cdaaabbb79ae5651d4c98b30dab02d90abac7f2bc8d426c51df7fbc4d3de28d4adcb920f9194bf16aeef4a5e6a349dc06f789d40220870b167b5d7575eda8024f6590d27a9ed2e2710e5a02e4c2ffeac5c726c7deef2bb9f143ff78d500f0861fb5330e3eb98a2db1a14263899349d8e76b10d69d4b6f7e63bbf8381a0d29808725a5688fbea6b5a4e4412d1d7bda894d730d6fb4534a1f0415e4bca4dfec2cbe7bb55913fb5d0c85c151cb97d681fd50f6a19ce8e94a9d7c6850b4a904ec282ea8d5a71e5ece9c15bcde0ca1bba75228951fd488af40cd4ed946bbb23d0ca0d2ca64d4a9ef46ae698ae2dc80026c78f9a3a6eae52aa8905035d88b9c1b6adac39b2a9ff7cceb229e4e78fbc26b7dd14f456fa6de56cb9cfd5353a33e7a3174b344acd4261fcc1e2d5df455c15f4554f5fc71761c546d4b68c2f3abb4d09f0baf2b7c2b71cd41b7fc84682ba1149d3460067f82fb65d5c3194dcd2db888695a8bef47997a325b638ba409e9f507bf42c5dad23e9b39fc210138670d39664c2e4018d976d1c2062d67af134235ebb40ec62a5b1b465537a19462bfb23347c3c4e07c0ca177fb98cfe0976e1b0de01674394e26d9ee77dbfb365d6ef773fd80553de20896d9dd1c6f089191e797f1556214fd8064c84d935c8523fcb81b93fba092897dfea4b98f735393f1e6fef2ac939026d98f498be96b362bcc896d92009c60d8a616f644b0f1bb8bc4238fd0013f13e4fe209611a5abcf668661048661f88d117c86fdab99c8304ccdb00ca33062cd308cc3b0c95fcd3a164326d87b2192735887d6d3ba51eaa38bd8a63b3be59c15961550a15332ccc36acdc8e6479dcbb0f175e7b0bbf40459067469a1ca2d3bfd40de281856efd429ff9689b4647b446b357ed8a2852c4565d41212a443a11ab795bd21d7dcb70321b9b1c4ecd96cc680a6c62dd50ce0b168da95e26a5c53499b5dfb1053d5de367ff405596c86d33b37c25cd73a5299dbe02e841f177020ac821dc28546444471d9bf5c167c36aeb509c9b047f2e618aa384c5662182b7b39d94e4ace817e241dfd6a4ace4d917913b74db0930656ec5df2a9210f7eeb0e200b93300c9089fd396e16ca8c35ef0994d782049b7882eb36fec134da12d0183811d1d8159ac9a5333b1345d1347b8c4234efa12125ee8a59e5a79fe38681236c5ee6cbf569eaf228629408a989f653134964f75e9e568e0ece1d2de6714af3dddb91c9c7f60ef2a9722a4dbabde498101f38e60d694f641389138ae678bbcee97ae101919760ea400d5447ae5f6013194494d37d8ee11e533fd8ee4ccdc03ff7bcf6666b64901d70e28738fa17c644c21798b936f8af65952dc5443aaa040a1f4292b83c3768c55eeb1c963455da52ce38c9ca6e834aea3bc19a5394d9c2ffbc0f20bed87d54701e50bed498282c1d459d13bcd1cd2a9cadfcd9f2764d3ee321afd0dfb9c8a48cecae10ab641c9f327353b28e2239b633b633c78e1e26c58c3f819bf9bc5085f6000e59ad3a6f1f0d07b60502b5e22989d82aaea2fb41fb083e6414a3a62a3826087ddf8eaeeb799e55f0bcae2acb9ee77bdf4db7a269a9c2cbe52024e4e60c210e2d774e137e700107175563374b7c7363ab160bfdbccc1fc54b465db39d25cc59f7f802260e496dc45fdd49463fbb9d25edf1d3d9ac15bf9c331a58b8fc248330f04ed5a5f86bc8c9883f3c10f6c035374d95806f52f02bf91ccccad5a78b479f24e989b1375135f5bd6f5634c7673bb6f25aad2ee9873e5f233200f6207db8ab0c3d07143dcf19695b4ed2a113e77fb8fb42674961ca57da131b1de4dd1e15e63420f574d94ae996f214bfdfcd0969e82b3f2699d57d7107ddca3f0d6e548e38e21b59cf7c694278d32c3afa068bceb1f71b4f0a4f50a13d1ea1a5d4be6dd4bebd97be5d96bec592be1d92c9cab9819c38d9c3df363b762c75fb1c37b3beb5705275a837c9392d71da952c81251f270453ccaa477fdae24aa15e8e813ec8b84bc8d0eaef296d28bcb7c04b2a429a0a6b03a9c661aaec414566f84d6c2b8910d90ac22566b1e3b9aec94061c6fedefa617a92734347979f3bf998d063010ae13505d716d52d5cc12a647fefb70e357767e30c969130f1e9203a4bcd5e9107482b1860db335d85045f9c6fabb8e13c70f34aa0b79e6cc9c90e7cc4420090043199662a48c1f63beb8a626723e9991a699d3409e1f138b56602765611d57b334fcf606d563b2c628203386668898fa91bde925733b19f51e626c95867fce09db98df8dee4fc0b62467d4d265387a4b5b5c60f7a14209372e867880b2926ef4dd3c63df50da10d40196b560186ed17486094b884c23041e51594c130cc7ffee7fffc47356f637afc8f7fbfe7aafaf7fff91ffff6fffedbff5dcdd3f1bfaa342ffff1fffcdb3ffef15fbdb11dde7ffee3dfdd399b8ff9dfffe37f4fecc73b94fffcc7f40765f8efd1bb6cebe6f8e73f7008fa33b66ff93fff716ec3ff28d223fd67ba2c439ba7473b4fe0bf56cf55f51f79936e7b79fce77954ff8bfa8f2cdd4b02fbbf0a8896dcbf6188611829a518d6f8db3ccefb4f48fa3f7c58c91d5cf34f63fddbfd93c53079e24579201482ebc70cc3c87f621e33fcc1b1e2486a82d88559dbffb374f8f3552a0cc3b88a44083f40957d38e0188651f13f46bf3f3cb417f5b41513e89ffefd59baff0165823f781257832bc4c508dce40cc370e8df0dd87f9810b5c5b735db1593f327a09a7fe90a7f9aac22f477188dc99d2010c330f01f5076ff833706bf8303a60411bf3f78fcf5773f7f9dc1ff89d1ac3965f230320c63ff21f99b048651d07c38289c459a210e1d861161eaafd11f533eb6d4e5e852a919628761a2ecbf02fe9f0d3234c656c19c452ec6306cf1870933ed7f9cb49e0b2bffc6e78a118161a4bf87c17e7ff815f59af49f928522f6875fdcdcff8da7308ac2a4f01987f0c0306cfbc7094cf1c7d436ac94f08d88bd72d46118c9f983c4527fac4e3009f510c86575f88397d47fa6b83fa0ac0e60be8bd36812aa23c3b0e3f197fa5fd74b055d44d0df5ffd7bb4ccc723edf7970a2f50ac70fe1d346d4c13130c69501bf1672e665906ab6cb0b4601ed0da805b9654e3086c4853ee4c350c3357bd50a7da47dd95afed2a6230d6d3b70a14a0de2d79b62a7faa86206c27842f2e7b650223f52a9c142156dad9ab01fc3ece01b5fc32922edc2004b0432d09bad07453f46ef08ed2da12b058ead4e9bffc4988d75f2fa46c0f368a4324b21d1722894f082abdd2c58ec41f189bc009ac457556f6fcfe97ec399a145b2e46819857204e63943e4da52dcf432d3c99a2b0e222380f2b989be8b7cc711892d2ff1c63f86d0e970e876987638c57ace041c4ca1e7b9e7a620b41fa77060c3b884c8d14c8aa8a21b42b8dd06a66ec6daef6543b3893169e5304e7fbc00a4f5f045439987c5b05a6250ec3b8306890b3d11133a0df0050ae648636d902fe600147450047b20467b99041b6fcb8ca734747cf8539a5cadc2e99dd2eebee9eb68144d128220eeefaa2df640e3f3b42b712f0e600a24b0f37ce4eabb96bf0fa78fb657dba60632a9e07ec004f29653bfa9c049d8d3f66c487f886774d79cbb5d167bf71d9a53f13affaa837a8a91a03bddb3c0f222ba46f5cd9154e1be68272ad70db64fb990a5481e4cf107fdf707d746013817544c85a4585f03bca21a7a30d452d322001f2e1d17eba9c96417b969a79edb6419ebbcd8c3920d69ef91897d53ab938130b37a60118f4adf6d87c1b7e229608764649dc2eb27d44c26f4356980339bab5b4141748dd96ecc9d6a2491a1b9c341114db9115590bea0e2b885b1b7aef80f48ceebc4e02f91de88f5c1002ad903e3013ac5c3ca26eaab8d97e483a477cd550b3d403af3e8ed817736a4401af2f1e353ec4acb733ad5fa7613d996368903a776ebe671b2fc6874c87304ec92417cb9f09c664b752d530520f483d74d3eaf0f0cc2848cd95375b520200314f99c4288df1c1cde8de0db7b2e30d3b094c29f3ab99aa38b8c025d96dfce9506a528f9a8bc1826d7b6725716044e1ec2eb7c4fd3caa8d0f1966c9c15c3b98c587edfd3159d617b14aa0811406e5deae98355b609ead74bcd1fd85387c67a11a769451b00eb258fe3c1f4acdb29c5f9ff552db2693a2a9526394f0074f119095bae7f825316a804cfcc11fff39617242de44aeb40dda995776033dd6c876db762d2212010c79b58cfe691b26663fe84f7dfaab28b53ed3d1a85b428c822d345ccba0648185659006fae470eb3259be9de116aeeecb465b6233d667fa8d5536c6871585f0192a753873662b22fe529ce26a2c9cf7fefd42374ab631e9e97e89a216e2301ac10ae1b4cd5574c9639673646ecab65c75f6f0f964661fa416c02e74490168afc0dd14baaddcdf36d3edc770b0ee7043cc1acb2eff96138221045910395cc8ed7f9f5ffbc3c95b246efef3f8e09ce96ff2b99588dad3bddb47724d28f8b53884e44dad179bb8432d6c3a8fd5afeab4504e1acd97d85470514145122eb2b46fa16c0d641b8699c94a59a1748fea1c65ffda0455104e0f6e2ae59913d267469a4845ba440c2d6207be422e7aaaaa01af90275be9731a93c1e875e252363df7d93dc2c4cf16d4a35711e4b99051d291f942ea956c89eb94b74be666104567c8d1905f0f59127521a898c5387d4787e62729d8ed3909c66b863a7c8181894e74048551ed0ae1946356e9cedb255720f6b5d44962c2a33981880db1205abf0d325b10dd4be0a4756b7e186906bea7b2bc64555624de5f2e49f378790d880dd32a47ec9017b41445b20b1ef2d62b14f494d5249c6d064764697610fcb189c8d7894546721d095a1a291beb449b47b7c06b0b99c0dd676fc89f541b223ede372722753f3688f3542063060ed78b1e5dcc46622a46ca4aa2e9d927c83042f37a2ce90b782ef4c0ca2b95aaa31f25fc176503e11910099ffd2720c61413addd6028d5b54786a1bc1e6d3aca42e50740a05ca99d08036cfb2194256591c48fea4f026b1e78393d557113443ecbc35dc5a4ebab43eaa9a4d875d74d50243219eeaee802d3fad8a422ffc66a6281864ccdcd0e220c89119803e55136482a28d1987b2da8736b99d4abe8b2d2b377db379aaaab730f19d1aa100f75eef5ce1cc8140c0784ae80e393b80d12dfb7212fce76b69074b31033a1a0c07da2f8ca62ff2cc0109908008e1e0460e7297f9748f046146aed1fd4b5c94e7fb86c8064486652f700d1a71b05c80a106f7a94fce6ebf9520c4a7251c68353f5511368ddcc444a24e88c14b600e6d500184d0f8fa42c25bc10186a8ebcf2f14ea10f5f5d622c9a2f25a55b6d66389d893742f4c33c5538e34f2c086e5195a3e3025092d26cf668c26f15d412c6ce53c1d190aa07a8d466d15595d9e6f267795e97b82f5b4066a3b4db5bb7ecfeb0c6e4647d3f867efea6392eec9f6fbdaf0f8ec0d7db7d7f217ebad87d44edea06a206ce2896df26db6178aa52ec8bfaee6badabd847c34d9e3a03f1f5808bf79b7d455cc22599207f0fd518fde5f5c7aa6710d4b6532f796f10967836a4082d1d6c3d6c0d9a36ce8b781e9879afb592bdd3e1de676f47cbe2d6dcf5bde1e7d83181f9bd92e0d3d872dc4ebfddfd322e6ee4faedfd6b33dc8d79da4f002e5c88af8ff89408b2b85f8f3c5f773aa7db1c305fb6f303f30afef25a8cb18351d4c868dde59ed7c068dd8564dd1e6f0ca1fe29ed8fdd936c0e85e3ddb860d512ad3e71b6d7fdfc95152be976f1e976d178103626708e527eaf248b5463c7a5204bfbd26d01f4c0b3f3356b8294f9065552b088055cdb030d774a11e1d3597d561fb43f364fcaf302f678053b7cb60aacd35b6a5741b3958cefbed64e4ef6f41e3386725be14927c38c11daacd7121f1494bfb31735b10407338bceb8dbb5eca2bac67d4763ee38481e25d50b417ea88dbe1df05067b124d41b0c5edda026b0e97a409df9760633e288d7d5631b78c3fa0a0a6969ae709fd31a3308fe33011d84389e9eab5abd7381abeca1e02a87725d9db3dc4fb38f620bb552df2f6253bf5cd0c1224f73c48274ae023a12e072ec149d545d8c087b66243e7dd3748286e5e6f26beb7b224bb6b6850a5cd50091f1eeafc54fc442a06c3afcbcf48df91117252e93356a106d4a92e7ddd1ee3e7d7e371548032356315fe7e37600d3dd34e86a473af2cd9fbadfafc1047cf45db5c7c82b4bcefe498ddf59ac68aa5779a065346b72acd33d69d69e419aaa6f68a11f364a9b8d27f421e39f14c73196bbbf78d9fd52ac99642a77a5c3d5459ea6ae81b3950e2302957bfbc041064f8e691c6d2b6cf114256347a7ce0c7a293942258a1e4d252203179cf14df949e8494016828d974dd434add9ab0a17165756119b75ba3c5e81667820cede594e04d0b9e79f561143b5d001ef3becdf327e32883d68e8ccbbf39c355fc1a848acefe29c01d3bd27996d2c314cec34d86df0b08db1ecee924ac0fc1eb44269d4ed73496d809d641e90b15f2b5e93baf163c92a795c6cf790cf19cdf125b566d1cd81f93fa9cebffef663eb64666bc94924b66131e6749efaea779e2b9983257866e14516711ea3625e9897a9e47ec64d62886772e5da3f58571035c7c759cf174d270059af15cb3846dafe5c2bf4fd4010334d661da258ec188971838f60ea99e31a853183ec1ea2ef6efc8fea18c00637f0ad401ea42ed002c90e902fe2ce27b97980dce259b02e48f97ca8dd96d9b6ce0ffefd4e9924e1f6f21358857a468811854948a1afbf3a40a465e6d83a0e832e41f81e3bb3062fed08621d4f6c844078726750b5b04fc2df80b3be3f246e1098916ffe3cf8900308ac6e935710e3e77fe6c741fb963e4766a1bbecbec524c2395adcb4ad2354157d1f551b5c8da9bcb3db9cf26add3c9a2d06283f499d33a45b914afbec41175bbab8c64f5cbb70830932db65b25b5a343b46b23a43fb9a269494e2e50791fa1978d8fac9433aeda8632567d3284233dffa7e1e4fda2c2139c2faeb942f755e97fa85d0c737005e5c2832b93b5a7c426eeddb8d5b5287bf54bc8e649184dd9195d33675ece35f004438cf0fa788ec46f9bd6445c7ff7c87b919689bd7e56f39849fc59de10dc6519883e108c65098e7e63dc69a99d1e169c68d1994e13bc68218eae669c68959e1e66346f3198ce11e46bf998ee12d465598c7e16146a7988de14e46704421ead22890c8df70b0820999fc6db18f59d75631fb3b3e3acf7436ac3309700cb7097f8ab8d32c26aab92d2ab032c4b12e83a6db33d673ea2ae31d3722d4504e873542676eada640d6777e3945240674697dbaa04478d93e90f7bda117ec0e21a08f2b47663034f1db080c310ec590c3f1196963810110d3d9e57207fe5819cba7b3b7b7251f22eb608516907441669500f0aea86f8f0ffef7a34d87e0cabe61ad34cbfd93279fc93cf7dd06663dc042e536342f5e4ee5b489c403b71cc2914ddff5d085b297371e197f7f2ee323c7c1aa1b00737240a182efe1bcbe6c0232aaed0bb6ceef8e23b51083df917f18b28bc5bed973ac18eb170b32629861e9dc255f53a3686aa86acc3befca48bc5e02c74a3f999971bcf854707519c0afbd677b54afbbba66f3a567f9933c32c88fb4415b3dcc2ca48afb6a12d910d1eb1974ebea94e698632154c8b81d19ce77397788f0a6d9eccbbb255cd4c90fe2fcda8a9e0e050a4eff9d88e0500319c07c27869cbc0008fdbd2fb692c136592da4a219b1acaba92c5f7594df46ca0c8e951c65b23125651f7801de2c1336f79ea6545bac601226ba81f316adad7652e247bc2a6d817b30ffab84e730dcdf12d205820c4363c806d32cc2f87091dda83a25d158e8668726ba30320c7b203ed4f635ad6d1cb779ac36b44b540e0a51d7715f569ad26adf7887dcbbb633a7d8dc47b2c035ecab283d027b3a72857fe4822619b3bdd709922008e2f74e9e6a674f937c9d4459e4dd5910f973836fc4a82aca8cdb6c72a7cc4fa3d0ef162bba9d92d816950b728be16eced97cd53c9fe41870545a4275535a1d0ef7caf8a4814e2b812a68b9f8f71b4e13501fd71eb17a3665df82f5c87dbf0920e4914c9dc9996242ef3b29628653bb377b67ec3955f429e43c80b43d2e998869734bdff0b6888dc0ed7c312b64bf30538f064dadbee12f412fbfaf9daed04c04891bbaa60edaa1c8f7bcb33608385f044da36e40fdb042b351b18663810d920a049a6198848ba756ec5701162e758dc1d60b917f4a30389f6bb906b60f7b2771aa74c4499fb7f7017c2d103e820fe2186fb359d1aa5cf018e0e5381ded679a2768003a0fa90a28f1064676e013963439138b02f0021b152263dd5eca97df62ea25e289ac46dcfda31414873ead85c2ce60b94efef8988c8c1cefc7e11dccdfed4f89b542ac8f852f6c6638530ee9e670ee29584354773bca93d47b8b85d4d23e1b3d026ebfcf24cfc7e41b7ccfeba5c78e3c41fc0eacd5c37ed9ae2a7dc7c5a8aedfc7058f00ee870f37bc2ac52f8600b12b9c2a29e79d3d0924e82b6adf68b83299368cd3804445b8473ce4399164f80de5ab4ea7d4913f9c096a42a72590cdcb10b155c330c571bba471ccfeaa73c93246828a9681ede556288ae9206ed5366681a24f80de892578024cc847dbdd84595f880323386265c1155f7e29567272fef89b122b8309590581df3966f588fbe2c5c944eb342e08723221807afb43a23f9782cd9b01ecb22b15834c44d9f6f9a323b008b3ba9ba58a479f1db3ce2a40698b3c606d6db09e27a68e2a0b968e918318dd5707490627b95eb684710aa14b6ca60ba3978746b5e7d8b8369f445ff01178cfe2d6dfdc43820ef563105d43cfce5b53dc3c33eb51c0a2af7dec92e1b8689e5fbebae998efa0daf6823ee979db9c0a89441b74c740b715230c13683c6b3027787e70b164ea4a0303bc8cd94d0c45c7eb99857dab0e41106f2a07805ffc33fb7663604b28528dae53470967fd85d2d5ab2f02e2b496da74adddf419c473ac060231f6b54ec6ab71e9f41bc51ec0b70739fff3c409bf6c69c8a282c5232e10bbfaf9e86247bfa70169e7e3b264f1a99cfc78120706fdbd131382e717c0e94d6b9f8cbe6c202301d332d09bda915f03df6bb3c458618551847f574901306d0277b0c400334e17eaee66764be777fe3822d200693d0dc470fb2d0e9afd577b8b76ae101235e2aa7d8b36302b36481fe46f7a206c7d4aa360fe662c7a3641951f979a6ca5c70260ba47d5810ab38036fed76c10f35aeac75c3162787bb48b794b96ac59accdc12fbe278652ed8b69d0b7b82ab73eb9dcebc23c9eea1eeadb1a54acb5edba68bfd5d957ade6dd526839910fc069415f7d502f0cd037d842cb33045108c32ca270212c354646beeeac7dcc2031dcdf4a573bb28a004dfd3a875c10f22b318f2e586c63000dcdcec83c5020b4ec6ddbeebebf083a3b6e5cab01ba9a5a3e426dd45ea00b8eb20301e71cb42dd93b888e5cb05be17477fe661e747ea72a323635dcb5718ddc365a283b9fc38c79cabdab2343e5492f704d0f336bdd7a6da77addb09fce80bde7b80c315c2ff92085695fce90fa3bb23dd47146c6a5388a94d6761ca35dd745cfbc5f18822a9369dc2fb37f81e78e31ab06096035cc3643b3e5913b3e7fd001abcb237cacc352dacc8c5fa7ac5e6b5b33c2c6baf85c4f38085cf0483f08a8f502630e6d55e5c8961f7543ab810569fcc93c55d56cfdc2e3dc27a9a4da282210501ffd3d1fe67c309c3d384424f94817706250804275e98a66d6b8c9c3dc566529c90521ef123d441d05e00e81e3c64f6de8caa4030f4b7f673876402974434d1a2bfb24667853686cf34db4b433aa510cb43cbfd8f9213e837baa5df1a1929ff92f567449b6716a816dbb8d271f40c73341161144f8d6ca51b3ecf6da53d34f0cd3034358c04f125dab5521fa49b87d8b6eafe67b0866da5a18f62fb974e14ac3ad1a50850791d0163a82d61fb83916dbfee19e9d2aa13e6c450f3a1d0f70744859744349b49d326c6bdde64b29cf538eab9be3e1243c24f633565bd4f0f1067eefe61a7d5f50c1a8ae57192d27029a1a8c22efa77c61c547e915e261e9811ee732d0de6604a94785e8dbab6d5772f526d62017b063553de5dbc298b2d324beb0fbce09ec8dfaf4c6cdf49a8eeb322cc81a732537a7222e86cb50ed2cc88674fb9db0f202eb6bcc9d5eaf9f6a5730068b626d4f1fa581d6cafc4818cf97d20a99a55672e46b8753186e45eb441b2a38ac64f86b97f372477aef8f26acb0f0cb736d097e812486d23238ef4f12121798d850504ef9eeeb7ae62cb841a2c5c05b4b7b02686b71297ee2ad0afdef4ee1588f8619f7b0a98d85c391971fb35e8cbc24bc3fbe1fde7b8f17809f4283ab816a93050877e134ce3890768053754cd695432a619e3193ecc6d0b4a9bfd51d1ad797354d4887d3891c3e7206c54dacdfa86b9b47d55323bc2460ff66d82d69dafe41f353b560b5dde28409e093ed6fad0b9e278de175377bd38496ca00756d5fdbd340920dd325535c56c430209ac14801aa37f6eb36f1cef4317337f7a3ebbd460edef067c20ac1a35b746f8348d75f431301030004c0e842093ed9208d5fd75a306c90dcf381104d37c65ac7fc71741e2b9264f3383363557fea6f85e85f8ad8a07fa5d36158e0d9850cf73e8c44b1f0d3dc58a1ab03a429af9e0c6659288217ef6264f82be2cdd0f07bbc24f4536c2ed0b315623022818467b337f276536b43fc8548a7aa2e146701498e509202ca3c13f717f86dcd7917dfc6cab860a7fd08975b4674174c9cd7aa119e87b31000d8cfb69874a60360f9a02cce330c95de8c7be3c16b38b666a9589b301a4faa75eb6d411dbc4a2431cd5dbb7d2dd4c89df12fbf7c8cd6d94c8cbed3b5b00d4650ddc294aa54ee74a33004c190588232f789f7ee9efd56248d38d71d81f86965340974c1b8f987e7e2ea57b1428fe7a9cbe0b59352aa4ec25629d761655d26236b8ad972725cdbdaab61abe543dce71f81170cd1bd64a6abb0e20b0dd1dd7a754577940ee018089622414724e0572b82db1df412991dc9d697ab97868196556deda78257b094e0395b51d2517762ad2ff2e43c2898f555dd4cf51ca7f2e49b7b75eeca72bae07a770009a6c22d2560bc999b0b7a68b0e3fe04535dad10866ac852c6dfc89cc258efef5cc5bce9e2e9590679c0aae729a7f2103c31fe681cf050fe1276bad6c770aceabd22294a31ff726e73b86b0969b9eb5138b967ad9507c8f3b4d9c9fa3bebee3695b8a0b7d76c59703087b7e0401020db068300c8bc9f0a543336bb23e0d2d2a827dc0a1500960b17030ca8cc139025b7ba143e28ecab9209a8c86913087f60509de95148d1c4d72705b81975d4b24789f65672a67efa80d3fa68698623de63374bd8da10ffde265e31ea93137917e01b10da90fc49331b27db2a240b436a96cfe8de593f41a4baaa52b42ccbee1f2474acb7f3c23a1a13198e0542128f8682821f96542bdf7305cf60bae9b85e02e0f400799399949c068c695861f7255e14a9ed899b2d9e4b183098278126db503bd58b29fae87f4ea35812b440b37c96f94b91533645f70aaf03544cbafa4d90fcb00286272505fc2691b050a7367d227a883d391c2737180660f17cc62f8ea757c2833ee41ef6d60986fa7c12d10a7f52ead1758b15d84ed70ac9b5ac67cb58c5dec707dda0e526d61992cf59edd4a60d58a174f54f2753203d4712c5c9019c60c7f6a3f0b781c01cf91b791fa2074e363a18d51a642060291f5db8d1c9121f5315da4d5a7947ed7cf56623967458cc5f7aa21126421af4bc133ce0ba83fdbeb219ab80eb2535e968fb9d0e8e71219e8aa46ff9b080f8d43b3b3ba085c3872631a84646062ade8e58a77ddea810dfbe86a78459dc85d9c7259d812a36f8a43198df1b0867f665030f305ec5580391aeee4021f9a54c22b1ce858f898536ffe27e054eb501fa02514dc1fd52e0cff832fda199c47194621338f81014634155b974f6c6b79d5f28f9267b77c7c08b093fabbd738ad16392dfa5446d5b21db47d275547364e62de3789bb920ddf9088ef144f4de929cf3e6c3701eeb140885b33037e836178bb3eed7f80951503322309cdad8d3b9381a133db29e8536f5ad7455b6e4ba852089940d7875268d13f7cc69ec1c9da5f72de35c1b896ace6e2146b7261cc307cd79082e795b731f2102bee9b9b6049559a015dd2149e70640484db8d1cd032c09308715b9ddcb5546ec589d633499d37aa9cd3190caea93c9f9e18065d0069afa4347a1294d4511887fcfa68a304cd079ed5713ef4edfea3e1c92771976e325bc410c8b528e926f6752176bc9dc2979b3e278e52322cae8c5a4b5338b03b32c3b8b7aa8302319632c4e17589cc95a0ca1cb991ccb549f22226b178a78b3d8fd41d9eaac56007057baeb2144bee737f09c1538aad7e8d330cd83eba146665959d5fa94b5d1c46ce0b30300b947c6c7096e119e180499ef87a5d14faeb308098061045b762166536a965a762f973d95bffcf7e0c526bb9753d118c59f37578d9078d92f6331f4d4e1f48ca6129988709aaf7af5f325503fea76cd1ca73aa2b25eab6c945efc829b1f7e1bb11665f5b5160dbed576ac87573dba591c177e2e29a3c63ce00053508894eda9fd3c624a791bd32464b734a9219265e93f035b39dfeb82c95fc8cac98abd1c7746e5a09cca4d1f21948cc6dadb696e03b8d59dc3b13bf05f9441301e9b72a7faaae6ee3d43a69ee912d4118e50ba7411b1f2ac44b5ef5dd6cc67ee9f514d866d0dfa9661b671975435ec73a1ea622d06f64adc53cf0b01c07331b2506aba7c2674b5391c1cdf87ca8ceda5f75f69df6f352d6dfbf100940020002c5351419b2cc3084e5a2017e53c49049f126296f85ecc91cf92765f6644f9176c26371bf2d0bbc7731fec03fdd0bb457288a89edbb166ca540bd0971b77ff9c6695fbca25d6ca868e3359fba065e68805339805b74c648daea8ec7a99e8ba7bda8a8396c572e1f923355c14b40b17006000035cbdea16a84c7486e20f4d692528e6348d94883cd9f82b8d38981d3049d3a8eb828f7346c74dd127f22de06d41db4192fed99fd5ee1a53243e55d90ba0b4a99ccc959bf87495717c561031f499e690f462b22df56834fcb27417ec40d8ea253a622e67758d7946aee100110df951de77913c83ca4a94999aaa1036401dff2ca78aba413f313c25c941fb201fd5477bc5468338f4007648551e4431609e4c00c44323b9a96af75fe509deaeb9bfb64d3e961485d5937c67da61060fbc2e9254a2953ecf3be6467695a763b3f64184c983b4ef518699ad1f1b5db1581195917acbdea5448aaf5231c803a9ed477ad413f5d941eccb69235f8c36180f14400ab48c086b0346807c4fb53a2f24137ea9ad5ddc3ee95fa111edb55eb5bdf6492756f5a5964825ec34a29fdbfdfd021fed1e524781df6c134f5b4924e9d95d4a970e8fb512f94389365a23a014379a26219e05c917ef92f6d2e0e3a3dc6c43c157d4c772e3c73aebba6725b7632c6231850f14b4f8fb42f16a47e4fc293d81bedb9d796bda8140477a5387b7104c520a577e65ed63b56e74ac8f95e3ee28cfc57000b21692c5eb0ee9fe3136d7cbacf43854658a51a5cb044c7f02f6ebafbe4441636fa706d77438802979a24462f999b37a22be4b9dfcef77b876ec0bb605c37c1252f32f2d39cec400d16542d383c86d3f42fe793d7c7c8ead8edc641530ab58839b7f7bead305206dd105cf7b8e3b50ad74bfb8ce45e556ba646e29a5d6e6ae8df5e12442682c22ca03cf75c911dcfb81c859f5d48baf24f19bd06bfbfa4da449aa814400ccb9bea1b741c3ae12b87ece240d955465c669eb2b95b5958a9927c8c848e26068dffb94e8813566a07e56a4315348cf8d140883e2e341b0ea37de83f9f2f5328b06e2aa2e73f1962c483a973992a1318d0f9acadb7d4c38dc35ac48f5af6427aaa35797b7e4897c558f0fc17ed5a172a78ae323568a3f51349bc40007c66b0257ad8a6c041ce094c6463806b605a5014c4daa1401092aa5039a2ed7e4c186c57f478c5d2916396a061caaf584d093c4b86a161746e17fd95a3662e555bf2829d8807dcb4953baa7e09f64919c560168675ecad275e9048d8e0e249e410648c7d29c08f7ec88c3d0ef5a098caaacc12749c35db4ba7a10c75e76f09853046055d8265dc583cd8f410cbcd9d6aa341c6726731c594204628f67d9f533e1530cfb727c4b3e24613de7ebdd372e7198ba5fa50ad29b58d15f84b714787892fda2c5f04938be98073b8d8df8a4d465c32c3ea7a0ce7cb9c2b795488194f3013ad3348f8aee6161b23489b151dd892804a57da1d7637c8496cc791304ad4f9f4b289d0e6b9741d164fcc8e0673f1126ab7d2b1744a274e2658dfe66639cd3e1587a4ac53dbccf6adbf580d90384a6add12a822313bf9db9f07d719d9d8716c954658c489def4de7eefc0de01104b69d624af6b6173f54150eefb69f01ba9d1e407ed3207e23bd4d4ecff0022a51e53f1844e6691b7ddc7af48b2f06604abcef6c264fab36b8477e3c178fd28f4315b15e58c76fce70c55cb05788e671729fd5df9e27ca517e591dce1489e32e2204ec4ece142e2ff1bd2ce57dbd7efb3399e62d81dfa36037cf0e76c4c50c5f4633476231d5947971d05eb3202680d8e0d9517c446dced11411bb8fa1d52ac0248fd678a0c6f3ec5e5e88ef95fbbe3aaab047e437542abf9be173a4a52f1ffd969bba608cce816fbba896df91f49adacb87ef4306af46c23f5153beebd4fab9cb715572e5b8a71fa7f29397e0534c81d7796d7a65e08245e9d596d1ce4bc55ecf0394edda74e2a227ca5f655bd55765148fb8c2c31cdc826c03c55bf83e7dee9b75d96859594d635607c6127736701917c6ae9325e53cc5792be1a38bb75454c34539bde22f5c99149734dfd22cd3784aa2459549308ecd4d6a739b831469d4d1b8ad1ad2e1e5308d547b0d5f6849449be372be1395bc8be8f696438d404541fc8c80a572bed17213919c31144009306e04d41704f227cc7db55d93b56a0cc899ed0b5166647c9a9f226de9f6a490c27373cae8e66daa4acbe1eabe1eca9215f1fc4b78929342c0c02621921b6c2a6bcc0e3c68b2c7dfdb970e192739a2d327624885e9906f18ae896700af6a972b6b0001cad591e07884e958b784181214c88162d89feb834c472b9e390a03d92fb6086b92ef653f5c3f9501ba5bb485fb1d5d5b7710c153bb80a6446c322291dd2f4b1c9ce4e6a10611800967427142673ba23fc962914f1944daa9cd72f936edfa0c40ba2ecc6ea3edf86eca5d7dbc855de779eb44f72a7bb62afb0e53749281a52b7c60e8753aada7c07c9523b50bbc4152e65190e6bebd577e244704a425ea702f015841cf9ad610a1ab79c397def4bb07c538fc7a4e22bb7109ed0c1087023e997cfb5e1ee7fb55966f4dc1477a2d3e8e6994155aaadd1667fb82015b0caee1b61343368178d8ca8f0996c826c9c5d2e8402613184d9e4805f770d68cc956bb3c8b65bf569a57be43c3a7df4861478031509df84366f0d28624d1c8fa71824094cbd065cab403f4f803ace59590914703ee55b0b57ad679720679b27b8caa1091eaf57ceb2ee5a0a6e7aba5b716f75fc1f73ba2c8d96298429d16a253c83729bd2d3d0fb597205c25c89e67793918093b22a8446a03eb44ac556f0b49565e912603d9c281b69e2d47a50bf6f3500fe590950d9b300597cf61d0500342df6fadab4f407bec84e74bb1aec20cbd689a965d747a065cb2ebded1b8443ce94c97fc7c09b8270e6d89f51dabfc58bfbfa670919343d889a2f4612bce149daf9e98e6a4a983d4c442ef302e4a784d9f00d202ff93840a0a58eea13ea455d8add2fbbb001302b77404b3326c8fe4525bbd9257d2073338faff38384b64cb0100081e28224c32796166726166cee9b7feeaa9716dda3478c93a75209dd1f197e4e168198362461d55746556df2ce7d83d3b81116b560c1ffa31e5874269c28d7be7d32734ce7b2222726aafd0ee188cd263a8943342cbb6bab1c69adb35e38287112f4a27d76089bb6a0816a91020a3380d58bf24b384cddd2dc1d6cd08b3f0731b546d7d66b874976c9eb61fcbdbabb26fc6c7b403b9d8890ca0a6ba24501f3325e0f47cd489f54e360005207d90a1c49d00131c830a4c690db0b763098d09252d34cd5d7080a8dd078599895562ba2d7329f194911dcbc876708f59abf238a7bc753c20beecac0cf32212699fcf78c96d81e4f2169518dbcb40bd56f199aebf0957aa46f0eb5d886f73caeb02cf39e19f9c096c2797d2137b8ea843e3e1d8632f43e990563e74fa234a2e766be0765d4e9f497b1b3b4e9ca61c288e47de75bf3d8a9f44cac59b9825e74e29b32ea05887b193f8724281ec09dab086d0eec71cfbc6719739c76563d2eae52eb2149f282faffc96b0041471ba78009c12d8c3176be5caed3629a3eea2c91bcd8cbf5eb36bf587762a63662cda714bc29deda27acf95801b627ce68a9969281c6317f6af42038f5722618fe88578622a02f5afd30968d782b92a80c1ce02aedc51d0659afb5f57438f5661a8a153cad351ecfd1193e3cd60f83462e4e9e9f26c6a837bbf07d9d0dfe8f4fb8f14e3560c6d1954ca7359cffabdab6fafd6a52d41acd06e7093096c3a57945bb6d3284764de19dd1f17586d635f4e6d556307cc706500775d9989cbba7a3b30a5e4096fc3bcbe7c67afaea6a1c81daa3ce2770582751c980ab0f6378ad01dfe7adfbe4a57d543827b3349b9831cf19dc7e582782525b73f78da847e4bafc8edb544bdb65755e0d050e595e8a093ff6bd83b760a6fb698ce7ae6312606b484fe3e101030832206f03ed10c7632b5b88f5f70ef0ccb313246b1b3be8ce65d958648e6c2291ef6b42b556176bf9f34404aad6362fc25334af29cfb04a108550798eedc4d9a3908d5f382eef3e8196b6bee4c8205fba2bcbec54be935f81ef681ad05b5901c028b5fa5e87df4e57d40908a230b028b6338ad4e3dd87e4d7dbfa9f2bf684d4008e6a97b387580ab7089f9c965304eab718e7941b2a133bf492f5cebcf7ac88fee71c3ee56513f59fa823aae8e35794750ee3e281c31ba17611e725622a5350e8fff86c2184db296ed49444404de469c247d4b659c693861059e43eb84343e2f9dc73cf295f77772fcfc64222c7e268045556b8dcf25f853937e42a0bae9d6fb4c132efff52d58738f1f18b8ad064ac6c178ee519759c45451591a7c7e1d159120eca69619211fb9ed217cc11894a052b1809716a45083c1a0413bc303e755a591db05e101bc026be2f8072e50604e55862408307c97bfef339e92cd6488b9822717ba42d86173fe592a337672fd3503d6a7d2745f6e3e25d7683e6ed4939b5675ee77186e8140e1b09d9f2433bf9630a599c5e6c6be8f6d26446d8f7ce59313dbce2465850acbbec3d189222f5e6910c883ad24aa8cf9a0f21279e7423a331818afadf2707580adfaa60d0e8d2dc18d537c7050ef811026c57ca818721306043a2af416157c55c1cbb50da106796a9ccbcf2f2a33e9e53c1fba9999d8db2f3967828e6ff680096764b56a4a7559ddeb03f8d4902478b219dcc0dcf84289e19b9a7c922e6112839ec83e87a2bc0e5b1e6e27691704385a8c00d71828ca44b29287a8e025cf9123c995c8c7be35f89c42ee1f647c4a1271d1fe3417434142c3958c704babf4d41934fbe9c5110c0c311c0cb77e3c09aefc005fc3617c78ffd2f7c9f4c4e6062d7d7c05e3d8141eea634650ce7c3f46d992279fe2220d7990cbfa13842d6f05499823bf318340f8871455590a5749e43b765f168ffe28b0451672da8285935fbfac22682aafab17dabd126a393752139cf2e387d086169fd0d08defe365de67105dc4807dfded82e3634ad3164725329d23093d1774181fe4f7af2eb66151f7b1893c2deb2f41e62543d5b84d2a636b28d8fe8cae6fd02a104e09552363113789f82041c9214630d020c229a81b8db860917dfa9580cc44d705988925dab605a44b59d1a9df7516e2175b02d11dfd0a726237814693ecb4963921134c8928e418b3b11fcb289bf3a95a150d735533bdebbcde38e743d23da7b73fe44ab5fa2ff5dccfc16b25f56d8fef450bea1cf9661cfe73f9fd2d2a008aad879c1063cbcf0ab83ccf1c85a01235aa16cbeb0c13ea9208766796d90cee422d567668865c453047b82fdbfc1d779d88351f33bce6711ea29d3c1537bfb7061246e8470c2b298503ed5282633557200159e2e5be842eb03b578b99a73f8d97f1f3224efa000f5c8625c4fb1a37d5b20d521017b3888421fd909519278ea31fcc9a96a3077d8b046280f6050326d1d7fa6008274b00b89421b24e42b21aa932426a8ee0c5820d9da2a91274add6d7d12d7881c3a077e814adcca57639a14e543f7a00bd6d2802596962bffa5976569fb151831e93ddfa2eb1db0ce6c250b35f26f36e4d249439fcef1c071060b599f81374bdc49903f67bc3304f2527511f2fae334411a5ed8f04a7b3e35118a7d8b3a5c458c450498de0d940b091f5f17b8ef3d8263ebb1e527edda29a4ee2d413671bd18499c137b7e3672f9616276598a7940115dc90a890a4202cb153b04070815600f449300be2aa4c60b5b64c7288a55e89ee883ba3a7466bd54c660ffcddaf84df48252975c6ecd26a2a894ff3c6d607cd4dc57afaf633284acd466a1d7a8ac7a715f1c8286a6314325cd3a126b5eccc8882129902309755c646309ec7a14067c1dc655fff95e0e6f4a0a0731d3da5a302f0452674743bc051a2b0fcda37ae28630ecef67901337ead85c817f54a156474196e5df0d1e724f4a3310f7b925225811232774b0660b8eb5a7e67eaa80b1a53109b80e2ae480470eaa838f29923c56f65341de0dc303221f54f37e257c41cb766b5cc2bf9bc1d1cd4f844a3ea779e65c6dea1a35fd1a858f753b0720807e127ae5b6fc8f4be2e6ad18fda238b83a2acca041d8c8629e988d8a67160f798416d6b69d97c9aa3087f546a9e45fc1506859681a7e1e4a8a0a1782527d11a51b4d12f005641c40f1175b553a70167f69ab675752f0a366501de62e45c6ff530367a17f4770d3b87fe14076cf30564e9f9b340c9ab2ca40724f1dccff35ccd2bd4606c2667cccf09d010a3f69d960082828261b00811bae2885a35c0abe6e0e349bd07a15d35c994c5cad9edc2339098566ad5973b4dfeb075bba25c03d65009e80b9c1dd9d8b41b50b28e9847a288801df558909c66e005b1ba40d303e8e103f62d98803a09454bea3041dc0153fa116fafc89e78dd79c4bb4cddd776b4fff545b0ec1af1fdda6a7224c1208615f4fbf54794be7dec7c7718c074eddf2f6e88685d2717b9b3c5e87bcea57b5d8408c9da3ab3284140dbaa8c4facacd2c389f42eee205753372498fe74b41e17189e60d5fc78a4c26fe82811ebbad139d0afdbca2dfd771055dea3526a4d232cf3e5ce2e12df8136a97516416b3b48070ad1555bded3d9e96cde26c8eb5475092932dddd1b85756d005092d77b67d7d66d285e1de817924e5114e6b90fd1ebb8c77c4e9148b486533994026612bdc8d5f07c0d6e22084d49167be76240b0f279639c3c5c47980a32657ecedaf781b1c970d2a70d87bc5225ba0075a9ef6f801c3488af88e272e2faf2dd452a1b67a89f68de9ea5e672ecc69de8325f9d607e1ab7aeef5b6c521b5bc9b8e0da84722a5303447cbc6bc0a1a8f4295549ddd4b9ede107c9b8378de2cd7f7f197216ad5a85475061496cc4a9e3997edcf591916ef45366a2324cf36a7110a79221aa082233276d0e424321b7b8516e60afa43c3c43af1f252ca2cda680afcd95caa342a5ca72428b2800d43f8a7366531d211d1214f5c8aaac4402eb84e348065726f969c304fae457bb5d712ce67c998f7c6ce72685ae9801d748e156ba1cdf9a943ff15a32098fc3728dccb8c7c466adfa763fe4f6bc0c2a7e77aac687ae785f1a258d14c14d8525a6a2ae9a9f57b66b59aaceb65491c0fca6692394fb9b031810cc3de9644587a81d1135bbf67c9415f65e1c1f363ef6b61d42cf75e330c3c5831fd240add30c6e1e767e695d0bc46c479a13750c1526b7ee69c6ebcd2e25be88e1e1834b7bfc3d8189ea3a9da3f9e946625a12c3172a2afb50b75db108c827bd01ca09b91fa41bfa0a8227c00e3099813c716d5ebdf06baccb8299e3b7a8908e6126bf712f7294811e01cd5ffc50865bee8f2e944fb649beb20a719ecca1961c7b102d3aad665161324d9be23d0f0511922dac696040f3ea0af8ae04a2b74d108e7e1f48dff8140108648b8feb5b84e51798216368af01d641b84e12c25de93a7d397f8f44ce59a9f80565c1d94303375bbdd515007a3b3d36207bf96c102a5474ac6df231c8e8021d441937d93d593432ef69f05467c11e697be74350f87b43431a04d749edd1fef4504f4ec517e29ab1eb56452df6b7107cb63f5247f9af3adf6f59aa265862f1fc84aec09c310792fccb029b3539a914f59ca457cd6d9a26c59582d2ea70ed38436b631ca07ef637fd1e4b6a5c5199d10f65c1dee28e3753a53640b3d32bbb12848a56f8a1bf615335fe9635b5aa5fb996006d097d68343f64476c7fa59b56ae0459a0e5802023d6ffdc014f7e50ad8e812f9021e104cd01bfc44bd61a3800ab3576ed52b41149e5604b7c57616d9c06251555440e844388f7f3ba0e25d80fa555cbefcfd91373a6c5e0a348b14d78ad78ff9de5b84ae5a16234ec589bae8b889873a4d9cbe75de56c6cd3f2fb7d0ffdc3e9ec985396deb0eba5926ed1a4169578b566c0004e6039ba9f24c415b9c2e60bfb913c7327f9d4ea1420e767ffed41516dc544bb4e80d2077c391849810d7a6646441096347128e600800a0b182ea9253cf640d42e901ca7523e28e4f51bb5d11803088a4799a34a5f83e037f628d6aa34db1ffa6cc36e21541b6586999637b4b429dfce7e643d91efb354fbe84ec34369e1e72d13aff1dd752b13afd87130412570780652b8fc943aad2a04e7eb2283c84f1f2e01727b12887b3e4582074dda4c31c3736086befcadf04ccc612911c8239e36d5d4d452cdfce02e486df79350bc0f084f4c5337bc7030c5780cc4106a32d73b9cfdf503afb05d5e2dbe0c3ca8a20b218c13752800a02fbb23721954ef5c5fcd1b8a7526912f650a34c47ade24e5fc20b7a6fc508092c73ec2698e30c7b2968d1a1e7f2525e17778c0b35fed7771cab23c3ed22ba44aa427fa7a186bd8c87e10203d9ad6e0457206c1fc51e95e475e5ed7b1cdfcb2e24462c374df534e3a686732b65beeb63ab426b9e9b30a480ae99c89e68a739f3c20a94c3dc2ce2bf3368c14a5b247111f670b8a8a26a4014b6b22a092fc5dd7d82dd0175c098ad2956dc971345a6c03373dc2d0ef2f7436f6240d8f91b63ab4c4c48004cff6623d47cdde7ebb197290fad57ccfaaa85d79a4230123af835fa608c9df678e834cf118bbea7b35d53c4c95b86b5a4d15af44153ff08262ac614b697e63476826bcc4e84620764fe807803f08d2dd11f52d566a58abd0bab5d826a1a30f785a01d2d688ee49a0ef8ba0d030ed161546e4409460793c2a123889af6f5515f8e5a7d8a3de87e2cdf6117d6f4a9a519ad5827745dc499c6ff111d9ef5b17a283ee9b42448786baf522720df3c6b64fcaae223d3b7d0ec79213dfac7a9f7a905c2db14bba91dbf5db6b60e5da8168220b12b54af4adb934d16f95c662034a57dbba985114d1f642e5b8eac9767ac67e4032c219019b44fe796efac8fc96ce163cb5c15b082ea397508a5f50538d8dbd4917f448565ea71402604a970728865f659d961a86704e013acd850895e63b5a7a60654ed6e426e8317dc34082c82e4924dad133847e07415129267124fc82586aac787c86397f8d568590dc7081a883d1e7c79d667a7fa60cf4c2b0a0bf379c69a1a32211c54d3b87b9e2433e4e11ab0a27eca206ae664fadc5f6e802e78ca4cc0b9618f18e6bf36bd50df1d72fadc7d6666e4db504770871ddc424ed206a60db74c31b113ab1e2701f3040fcfb32d11d9133317c9d9f85ae7eae7089faebd928945ee34f7cbbb41a182f4db6241c111e1d95d7f94c41ce900aaa2c21ad0c6be312fec5e55d8736e6fdc423a0e8d6edfb23337d91d43d74e4dd4b42e7a5508377f573afa1cdaf409e441ab39988edaeb16889b039de8a81e7b52113111f6dc63e512ed40b0357099f191e5ecdfdba843c44dec1e946c3252426fe68fbdb9eb73c1f2e4afce9bf982906670991dbbc13cad08daad5ab1f81e6a12a15a8aba5d0a4fcc8fcdcccd7980e2b92b758f0cd9043c933950f04c6136aee6b7fa6eda0cb6bbb40ee62293c3346f6f8e7941eeb105638e095041dd8a97649595d1268800cb24553f390785b58d1ec915f09f2d2553c53af65f8200d1b4d008eb9c18955818400807bc59bc84ea05a373621cf6d1861a7406a889f321762048dd331595381273ad23f370de65147ec93c439d89b961744071f8d22a93715240af40e531a9d92933569495ddaf41f401a051c611b20f4267f2740dc6ff5fa099abcad7513263f879253ae4ba55b14f58a51182eb4a7c6a1ed7d1474dcdcbc1aefe7e4e9798d8efbf7323398fc8041e0ae4c846942ec13a9bcdc0a260fc4b92206223e6e256540d4c80818eced349db868a78389e27a3018c8ff082ea22ad90c0e9ed58037e3ce10bb58a8958def6d813e02ca57c3a68f9abb960f57c04d3b2822b0e2857287bc649756517e937cf13db08d978197f379d7f4252a2c91f66934d99b8df1712c053a308e7f3526c4f132c0a0455284911f5cf2647ec7617053830fdb25fd6028d7f497c5c04c0652945adc884ae11f9baa685fec1cd2e1864f64b8931aa5cc58b27e6860e3543e0ee3b413a1c86347281f0f904213e441d460161d36584dbf812c4e048d4021294a3d8c5642dfcf0ba497a2a035902fa5d70cae4b15ce80a0c383a4f15e78cc643dac7501af52f97a926ecc7cc131c70ccf0ad8e6192dcd0bed8cc918bea04648ea24a48e546380490167ff0137f49e42947b2ecba6480db6b287e8789abcb6579ffe4f59f79cc684c66ce1b9cdf198cc10f6c844cdea81c0319e0ed6b751ce07213d4cda487e29047259d079dd3a4601430b7c0bcd0a44689fb879e9e6024ce8e95a6db5cdc82a02552fbfa00e2d5270bd983ac6ebdd58922e3a844e4e841ee3194a0d31c77dd0ca3aa84dc18de973a580a9d7d2662434d7aa3c24512847eeb71b24275004750891181e7292b3a8b03946f8eb2ef642857bd2cadc6182cb95f6848d3d82a2a8a22a6a8d045337281798b8827e8812962e0f57ef0b2c70bb9e6350642dc8c1396049bea1fc37117773474543276ba7fd3ccdb50f219a0b9ccda9d9c63aec02ce0f7af683cf89fc88f239b2aefe9065d47e07923d3096f805c9acdb196b2a389c7924aac24646d44d813a7d86688ee99ad9eaff846d381416ef2732345f5d1045c2feb286d33766bca7491504d928ea3ab12141ef9a7ad5d65f7cd0297b61edc67fafe17e3a2050ea96f29f4ed4cfec813a02f4b13213f8f0c679516733168663327f2e28cc95754e99df633836e1a03febf074e5a09d1f5688fca80a1233abb9b473021bbc01cc4dd8c96b88b4f32828b6a00c8e89cecdc365f3149ca2b0c5ce9a28beebd49050f1ca733b763ff2518379fbde4c61056b934b802344a5436bd20a0f11c6d09237122991301eff02d5905bbae738f27b330eb9de65d05448e6398f23c6c9d6e11d88cb465c57f0ce9185944ad9dd6360cdb5c1dc26fdcf1e7c9285ea62f4341025d7633aaf3318021ac322f70189223b509c9080fe3d42a0a236950d7ea98e618dadd08907a02f841e47b31e5166523008d3e4e26d090a5cc07805c60a1f84e2d95af70a7ed50fd2fad56080634103cac7d55bd34a7495ef848e80f54a5dee0a8c9f6017637174eb251eac7a2872f262d530f113922c8bacc81935fe3e79c999a7f0a26b569afbf4c635ee6eee524d19010e7a8063f4d5eafbc71d8280f308b125caa29ad6a702ca624500b202d780a046a3ad0ee5bbfc20297cfa498b11cdacfd6946cf8111b0f4f8c733a7e0d89e89ad145989a8eca2cd9b25bc3d0b7f837cdeeefd231d79c8148e642288d98051686bf7c6efa53491c0e837e0359e7be25ec283014c88448a6ea63011e336d906657c68955656638920e6688a3251f3990cf5371a82bd46ab77b3716b0ddad56b7c69867a1d60f438b1c65088ce3363bfe325b6ad69917955b634a6e88ec62984cc82e25dd12dc27fa1b0f8d13121eaa4798dc0e279d8b5f33579789f6528b7d06fa03180b73d3f838eb67baac12d1e19597760009aaacb9728bb5662b867400b1a8bec8d1538f9a411c3304a8ddb276ed2549875115e7863d13487b77e74411e82dcb759908e1fb43b63e9a5dac7a47a5e2bc2e1c61c926285dbc554e42e8b4525855e9d0477a244bc914b0dfe44cb681d3fcb16ec2a7582b226455c8a25205edd730e8db8fbf970f634dd46c85768891d4366713898e284e2e926546cb2e08c102a11a0d0640101913301b031ede22a5804030ba37f0d4a5f8e8dae1fbe6ad52be06a714f2cc69db53834866634bf0e2dc379f14042730f8803bb105c779133c9327e597095b61b33670db84aa9a460bfc4193476c23c124b4ac013f66c1a9e954a4d7dbf0c8bc93a3ccc33ffb9ce6d98cc3407a2aab19fbffec214771ffb130385b33ea1384305a03ebafcbc84820e2118b36587131121336b7668ab7a384b04e63812f870027a0b5b10205c0fa22c565a0c58c959c69841c0976a61f4ac84e7ab8ccd03d98030a871bfa4468a6b0380f60292b59a6494a759c400aa3477b78b4cd8b194455aa30e05a50d6c5b8ff94e6f96d3316ef7d98d46ed7c6397f2019a84abc92b44db50c2133816a3c6188b75a935465a826790b832e0233090e2429500f072d7ec5f51e913ff1d49c61ce4211c2db45efc2f00f8d1bf62ead7cad6b4e208bd6dc02567a58314b11045c98fba393f3af31dfb362ea7182f67c8cbd647ce82f81b6fd688f57ce08bed7b19ce7442f3bbbe39779d4f737443b65ce1616ef34c1e048c0878fb83bc1fbe8d919f052ba9bf38db7185339448109f8c0481d88474e90568d04ec4bc017119365a16ef30ba913d8cdde3e9dad461d77f4e3c121018b0c4de2969a6ed3aac89b2a7539bf22995d845077b559562f9e41367e99ea0448d7d02d20bc68a0568761c30acd7e81ff6625bc3961fcd7e2e8233da21f5c416e1f463768a68c1712c3468951dd957e49fc57e21110bcdb6abe28f574755e2e45a7ed4e45213f389de3453e56e2f5ec350650cfc316830d30da9059b398c8671c51e24a5c690ea447b7749df8723dad3bf5ae423a26846d348fa23885f2571f5634ddc6628f97417e0c3d6637a35acd384e667cffca99339da60aa2da8497c7fb2689cdbdfc50fef2e43eb2e1c8e65095ba817b0236fa5a60e629d6185bcfd5e15c543b82929d5b16eb3e7f1c043c54cf068635d1b1384a1a0c9f89ed62a550605979e024531d247d0c01361e2395a8365171a82c9fc807aed81052fdb9c96d609518d520cd77531f15d0aa1ae54214b1b4b0e0adf288cd71d92078e80157f3f238c2af50cecea8ce3c3f77ec28405030effa4362d53dd6be1235a57170ba4b482955638c7842e6928acf7864ecb560b5e4675d35ebda307bbab96b339beceb585be61278da9041b5d223d85c90732b147539c2d8fd513bd256dd9aa0bdb2f4660559e15911bce2fc794fde02f36db982e4960f9890ad7709448f766187d5c6467a5bef10dab04c878fb0520365fe4570d54bb5f27ea1052057e2b42836ebe91dbd9790914550e591a44747440aa1a3691582387239fea6a4354558250cc72d0ad086d00e6a00bc9ad6dd0315233459690204dbfcd02aa5c719fb52f8a82b021adddc85ade15f8ada06199d6e167c16ad19530d225f092956b8a37618e9080faf121601b26da8fd0610c8703589b52ea30e07e04c2e380ca8f61660dce5a12f7f95db5f953fde022268bb5dc08f03571f9e54ad1963b229f68958f9525a8ae91da64eb93008a463478c5613bcd6e23e53f9b81b3b85ee31b37e73bb67cad35dcd453ab8a33636f3df8fa1b5a19ff7e918c17614e300d190a258ba5744ebd7e1e3a4adbe81949815c2cb752e215b2b6c066930ca08e23d4a0bb32d975fdc6b31cb1b24a8f780e340901f8d6dbba351f5175ed3af6c3e84743f55bd3b128f633bbd74ca3c79ef692e8081b74176ec10bf232464b24a8b99c21d499423c3108f6a07e2d885478eae353560aefdcad137a58d2deac1956d4f9ca66217664079c79ec9e89ada1ead2b8ed118d01efc473d87e0ec9eb3745abd38821dbed42cf6e67132b1029641fa434094c761bc015e1d0e80f027dd82f734afbd3c6531bdd521bb9bdf2c6388c10806571b918de4fccf9b8d488a8eccebdc0c31b43e81e7d429a5d58edeb3575140bf0bca37bf20582914a5536c8f470f38ee7b329c634cf2596d44a31515777be74b3357e84e298060ccb76e47b0066f898024a2cefe44bd425f761775164af22b2bba96882375b2aaabfc9df49dadc042aac408ceb3cd5582f7c17094bf380c425d6907f88987071e3c421e411a6e50e48c54415a99f6ef67ba08179e7b218f53eb89428de9582cf7de2fd3dcd61b41b06278a017d4d043a49ae0ac2eb23e33c24f343323ae639590a220c58ffa0216d0690c5e359e20f68a1c8d2bad6016cfd15b5fa28e9ab2561ab1e29806549f3f8a7fb0ad8bb600d6f52a192e9073f59d6c19bb022515db6540a412cbf0958d43dc5ba1e3e787479e03b7697b703e719c3987347156ad1581ece498dd9803bb9bf7704cf53b860e8d8c936a8c98962e090d270933ea40542104c29900d9d8bd017009c494ac125a34f9ae5188da0158e09a1abca2e2d067d1dd7570a729197015ffd42b4cf596c4a9432fec1c4a903dd450a36c5dc22f6b111bbd617a18101d757d56abe482acbfa272b5baab0703764cfb8b9720d626ef54fea9e03b1ba8ec231bdf35253ab3b1d197bb55ff1c2a7b67723f0587c25057a726d01cb99f39c588d6e02054caa794c241682200fc62782819dfb6ee4fcb4613069e4db64dea9e1332986184a792982cdf1b0e7b03bb9cfd8c331ccfc4620a09ff7f1ba7b7a7bbabc1cb6df15cb44940c30ece787287b416e6626a37d1f8a0daff40ad9c78ff515962f98b34f155741dc2be5d0174e476f63cce8a1f1a29a24ce0a4ca2784e3792d2140d643efabd9cebeb973a4b8f9d65c10411a9d8add56441132ae92c25f789250a07b2d48847872cd490643a4ece52203faea5afb826ae59a570a7e7543af34d98cb776a1d2f5ac31bacfc344a7e83ddc8308e0343df0dab7a27a80108f988955269a7ca3034ef90faf198f653b4f956e66696c09ce390d34fb47b117b3e899db5c45223cdf1f9ce0bcd157bc32d5aed0296bac8ae14dc207a7a89f440885e50fb1e3200d7024805642692b6d184e7aa6edcb6c8db24df4e6e05e26916c34c7a703853d2c6f83e26cfd9db4c46e447f97c0a74271b3667c50aafae7fe4b318e1591428d71c9270db2a987bd35aefdcbb2178258ec436665651771110c76913e2de4bea406b93003b524c9ba3ca2dd8b0a5d4bce5099b0f34e90d6b182b4dd9c8a3b41c214aa2ff1821498fee641c474470ce961975127febe1d72b16ae572ba207954e8f2031d296e0347ea34659d397b96fbf2856dfd16444c44a8b3bd598bc7f922d3af9204ef59181c053443ef74305996c9323ceda464b05ea41cc2c0ee08f2942e4d9bfcef7c7e25109e43345cc96a281dd1c8e22d1538322093b41bcd51132a7be38698990a82a10e4d158ef2dac200ddaa622b6c498e097c4b959bea1705575be6bc5f9ab6264cd7e12d89904f16813f9fc6e22d166906bb19e5ee487d00b4c90f821e4859f1e765b8c4b0eb41fa488e8dc91f2cc368c63c7d77937081bdb21cde0ea2591ca640cb7c683f24db42f2f2d97d64ef713528a7a89282511ff8e40747069100dabcecf1022fd2bc705caf8f983e42efc3c38f25576d3afecaeb21a11a4352230625e1049cddbb20a175360d24f759beb3ca5defc136f9c35c8277b3d32a5be87eae48580927534abbb6f7fd9a5e87935092540c8ca3531f4427b0babec6baa3ae6d80ab132a07726e0ddeffb20aff377d79e15d9e2574fc47edd62ffa6fea69b9800b3695e98916c0e4749276dc4ada95333b9ffec26fb7e05c42a47ba34edaa01906e9f47663051c35ebce418802094ac951bb8967a6ebdf0788ff9babca001835f0a9b7a5f72f90ce81774173c46b9a86548bb34cdd230f964ae8d4249928bd00e52b7c03600566b759255cd76686be7eb4413e9fb1b24c224002eb9c68a7831589ad0add9e7b4a60d323222ee25c7bbc08c6f4da1d7af94eeb8a5c74fd27b25d38df58dbf30d0f5925d23fa530861397e83cf298c2a708cc6b1dc3ba4960869541e942782fa5db79736a78912dc3fe5598960dc03b84c3b2b2632dd0d94c764a02648e2515f2824515ee78a9e421481cccfabeb1f04926ed5a1703f0602f573f88b0a65de47258c433a1332f04fb51ad00cc3ae944afc5abd9df23ce3ca35350bd3e529b71bf06219ca1c470a0b640eab1b1674ec28e43b3b61310322cae4d07483bf7ae020f5a7fbc6d582bf9a2c08ac8ca06cc8eac3169d3b4563aa264697dc0e56cb451f71143dcc66459586b940b6fc579e891d4bdffe5bd5616f87966e449d25fd245c34df8ce1fdec938d7f44e1e74dab9a30ed2db7d33dcaa4dc9ef06dfbbc2c9f32bbdaab3e7e638efab95ab0ae8f0e499a36a08f6f4f3af0affe793c2a1a79388c4f57fc883d128be9d156034d0eb595930fdd6e969e1f3846c860412135d972b535c63dd4bb727fd0719df2841cbff29c3b41cf101e49b382b49f96a42653d3b494ffd8e98b333c850040475830b433354b4f93ed664cd1dd283713487bbbbee61d0856327e3e886fba638f8b162ba09d7298f91505987caf8ca3a281e67a560e0ca6eae9a83c837fb0cb6d128942148f564356291f588146af4aa35788433701d8036c0d840698252e37720fc6311b11bf13066bae7bb01050b8831a2966edfa636d58290c3baf85d2733fc0663846b730e7a6b43596c6866be044d5033aec0df9fc7d395919b698bfda821c77a695d9895b1060ce6f4977d7aab90255265308bee6615624f27be83e4aba50feb27da64bc91a247faf50b7adc728023d7fff051127c1a620e705008cc21420d7a5e1529eedce48e766696a38367f3412fb5d89faf9156606c45f95c439fbbdd093d44be572a18333e3695a22b890f605de4713d13117e45d3a9edd77ef1d763f703b1ad62ce1a363ecbb057064cc3b737db6eca489d322ececc4e79f0c1ceaf9b12cd19ef7310889a869c3cf44825dc544eac354168426e729dcb4df01e85ce5d4e0e0c778681dc8a268de8faeb42319b99bc678128e38c21ad219eb63c81f028ba16a6115f43307e0215174ea494aa587fd1a2e00641b8c36e4cbc72cbb459b1f9fcde2d938798b13c32f5b78db219c5f4f87b54e87dbf1584007a98c411c7ad64f1f744988254ba933d5bf8cad77a5751637a5f5db165fc1033ab0e947e7c5ac77df87256293326f60942746db8dc3d54d3d3b152b8c1197e041b30ef5c7c927821b2216b5f7b3f3f6eb7a2cf5ea578c69eabcce5b40eb93af6c7c06fb7993fa65e839ac3df705116b2e6f22a52f5bfd840e7a3aa755d28c1cd29658dc528c1308ac6fce7d31c8f7a13b73b9d4dc7f2f8916bdd094f0edb5fe70e1b72998a43be98a6a22eff0fdce0da8e919cde4a6820922a22f7d582af3b8d942c4af2f40ce4d16972d17ba0eba4fec01896c12667f58cfd6ad859992341b5360785a5125c5a0f45242fa5755aab555f9cf371ebfaa477188893ad2f67ab4040a6430cebdd996dfee85557a922788c3da2b2a12b91ffeeb135a03aca768844ffe628170c75e874b80c903c5871de2171cd759c131c4752929ee815d3a3b1f552702aa0edf61513db86403fdf22a4b0989ccd6bee5e7879ad24bf3eb60f4d2f1e2ad161b273890f244372de5295ef60bed3e4385fe20bd6412c8ba0b64222426e7b4e3798ddc85fd65b931e0c98ce715b8facde19abc6f00cef5b54727058bfb10fe50e8589795409726044a33b7f52e2e90a014849d5f63b4ac674e2689980f79babff5e0aa305e11a6257f5fa13ff52438127646cbe3e2525bdccfbad81922bb9bb67862164b6216f4071330301aa37be7e0219b4065b81ed053b9f2b2d884db89d90ab7d9fc12f0dc87abdba4e1ab06f860dcd4227e64bcd80a208444a05bc3d1c2821acae7438b60d1ada0a4bc51a206af9f29be19304e2b35ef4f41522846eddd995252380b88c77ff5795fc085fb00a8622e545525a73dc8065c5144de6042b9a0fba01b3c933374822d86e713dc55185daf40c9297a52841b3280b1395355bde3ba65aead7f280b015b74d3d378a696688f5a1bfc192bd657f041bc47b6d425519de05343f646900fce68ee4cde1856e334380b04e9e00ab24c999f68aea75d837a05ec57565f2b47cd6908e265222e8068cfdc587546852f62a71df2b131df6703e0e0c1e6e0fa64bc69045df0144dfeb7b4d3cf0d7b8cea0636c8b4e12121695c80eb52b41e79bd89fa0839d4bdbec70faf839c04b55583ea77ad5f62aba8b320b44ff25b70c5a674ddff90d2f53df1771f1c53440a29346bd14325bf84414bb583a57dc16434443ea771d6238618364453a3b98ab344bf3be1715234adfb8f000631d50b5ce22c1e0f983167091105fee22cdd6dffe0625d4973a38e5b1cad86a0eb2cd99ee9efebe6801d81ef727623ff64d5c39fdbb669d26bdb049a98ae1ef43d882b17d07de260d49c6a06b36462233c588a9e00d1f7504975f0f7660a24e700cdc673528ffb4a5412417508cffd66d8b7a949388b6a4d0e24272e0a255e3f877c64797463877d579480e66e06202e418612cd94c7abc4516c032a9add2ed5a03163d45010db1020d4dec6617ab626d7a3a792842cad334c9cd3a6c73c5611b1eca1a21f2bce5554ffd269320145c8bcc5e703ab91571cd93249fd4c44bfa089c2989468f6b84bb4f56c83f60bc5b5dc06511dd4b1298f34be78c0fb41d3d36a2ca048ca24bc394635c4fd8ea9f55e618ecabde2e6139c609fec95b6727a2ba756be8ce42bf1abe369db0dd6b42132c20f9f88f7c5ccba45b2260c2136000f0295b28a873752ebc493d32cfa1b9844c06bdd7e06bc6f392d02aff4870e150998f29c48e209a6ba49827ed324d298e71f125a0674e3ee761e8b5aa207cf4bcf1c847a12b8814a6ba06d923a2b6a2cbfc26f324d302b2cb2c53adbe826b23c5b2c4904df59585d59d5f727270e6146369fd36ec46cacb3482b0ce1e72f0da2f4f1c998c9c78c2ad22790bb41256a2cb2bfa4ee8d3ded70d4e7b30bb0e4028be67c4cef0bc9a9aaa8c8f35dd2407bdf160ac1570b9ee96514ab45095ea1e6d10e12492c49e18a2ab1e0db3f4c55ad2f865f13590cd0b51ac3f391c1ea5e2877f7c3250368019c68077d75b82534765f260abce682eb443957c4167107704ec70ae1fa0ebb0f469ee2192ea4e7c5d0574147ff623fa3d1462d31d34c3b949fa11ab174b90de5dbb6f48dcca01e2be777cd00f48fb13453e8f1347f1899e4b1c3bb3e9ba8ff465c514635e5527bd9432a4af5c5b0298882c3f45321e74288f0228c2f33ebdc7c69f9e04fe8e3156640b244591f099a734e27ca5f315c75c3d67ecc11e6e1d3a32b891c6eb1442ef587c65c0bdc1ff738132894ca1dfe344ffe5527bd2cdfa7f5ce75cb01d2ea38f98b83aa7bd15ca8b51eed7af2e7515e9c3db87871a5298b0e9c20da1ce47e7fe3b4c464358d1779493a965ac3df2796c93778a399c7d0ae6b9c178a9c84fd28d35844a19829b1da28534728cc83b1413a50f5c8b988d8b91d66945de69ad2c9d3ac623108d60a92195a65a0aa71364ae6819a60899bc5bbef6f0ab10e2f261e221f7496015c5ff543beebbe937e60eab6efd5bb138ffad997125203ebaab03eca868db30a499c758cb4fa05bdef92457f3ceaef3702505f7c4df9aa84ba7286c8924f8ffd805b604123427cdf5df0c9e6aef110490e1a627c349a4f8053b8b176fcaea569d6ad37e9ce8d126ebe4c2d47e80f5f51fb36ed6c7c5361d824be31d3c8b4ceb8dc5817cc7678dfa99c585b1fb2f3766e8a5ed04a7c2e3d9638ef22eb8283380d8d690f9e3e2dd9a2d7faa7b925ee94e4acc35f630aa3582ec2f5f3a6b98fbca670e63e8be5f1d15a438d7145ac2359d1a13cd59660fd5c0b74cb820cf20903fb1f8c9da44ddc3e4799c18ff870aef9906a19f8f564e42f8cd768e18d9255adb83a093810351d7f3fb12706d7e933ccc7885b4378f4513544eef1ab5ab60d7c049204ae6bd8e95c64e1a002017bcf33b626a838e6c419b504ed9435d8c24e5c4dbe8d83960807a47e8e58fa272378721836732c933f09c836486d53f89bf3ba457308ddbb77031060abf9690fbeb86f5ead7a08e7652b000aba41567da895b2a1fddbd13e6d00ce4a87f88cf7138a2f138678dccd150ffc950560f50770123daedc70f1c126b4b63fba08292a902959cbd00d96503bef3f12ed2617e9b10bdb007b58b43700862aa62fb4d721e480ac462c44bf9ed4ccc766bbf5034f93e0f191fd704b3fdcd1016a083d60a5e4de6fcbad9d89e26e05ab5d76ec1ca1e0a9b15b109bf998afd60fe0499aea23f8a4a13156776057c28bafc05f7269969fd4164c4e5010b113fd860a1b04d357a7b3f35ed7835ef9b2c3b2d8744727fdf68cd984f374b307455e0f69698b73390d8c3b2cb78a5d5f2c7fd715bcc5d3f040ab89a76fa83656f770575266f2c585d0520d27ad084bf9f95e182d338634fc4a13e9c5b5e6dcec9cfaf038a5defaf0516640f3b8a4327ef53dbcd7e7477be1cc21de3d7e05914092ddf234c45f4b2aead79871a1fffb60efdd07c8707ac203fcf7dd2635f3c4e1071316befc923287fcf4437697fbf4ad2e8b33d84b6d485623872635d25edf8fb0d74dd07101b9d355bd45e3286212ca4c531f83f2ccdeb344efeb2481045282a8942d4251586daef6406f93bbf8cf4a3c2c5649628fb6144c0bc251c0f64051cb3e99c91d68e3b9c387da76a5f1451af7247868c286b23f540f9cd5c8bdcb692ae9fd6ce61f5fefad433b906409fafd15e5f50cb8036a558d01965a6b4d8f975a6bb9d87f5fbc57ddbdde5ce3227932f3c4c900898848272ebbf51fadd4e27d8477d01e09db15841b9fbe949d4f44e8eece28065cfcc1aefe8c9fe8514434fff2c7f28b4a30b47bb1218800086bb17dd836142fb5141aa016ab1cb2687a0aa3cbeb8f78e319596ea063bef4fba5f6f7c58c66b2989e26a810f37d22a06c4a5877741acc987e3e8ad8b7c86047a71c276edb1ee3554c4e07adfd288b26edcb5ad3db23ec03deaca92a61d3b12701d970a7f7fc029b545bd001e18f2280a782debcdcedb9fa1e1360160ff4fce7678b4bf8cb000bab3b709fdd86ee542027f4dd20ba0fc4675b18a66aab0665ecaae4c716fe4cb7978844cae0f1d8f966f0c52126ab5c79f4ebe680192c31d8c6664917aa284e781cb7102a9f5633836945769213d44c09e5078da299f9b8ea4d0baef6c5630d4ad5a1766b0ff6387a61d7edfa593a174e52561c369a4e133c0adf99f236a386454d59a4ab71093ba498e46b5f8c4243f22a1d0e3048a1b33cca18386330964360537be21a3f1881cb83dc4d47958b8102a954c51393a4dc5cd75a5d14fceac5f0253b7e9ef93bc11023dac9030fbf2b71b23a50f49a362c855f3cf190605de1b826ca37f475ff0dd63ddc6d267f250fc54621280ce0adab1df6575b8d0dddaa9ca933d424e341a84ea4ea07632ecb222a9af85d74033f4c63848e6db5bbc8a2e663a4b98d0a935d178f7b886c47f115c8edc9f7ce4d02e6fa112713df76e7b7d9d859303aad6269d1759adc1bd0371fb9546725137b9bdd7936dcf57d519863ec02eaf4e5b3622887c053aedb0c00fb9cbb44066ce7a7022ac9d1b2dbbe431e1e3290cc98e0bf311f6ce5a1814ebeb1e0cc14d39717f173d1f647af586d4ddf25d7528f2e804953c59783017df785e25fcc0e8ff5e7183dd9fc2c03b517dd70ad4bf8f8ecb200f02c78c582519c9af1c5389efe496a48cfc617d01005b6799463be2846f6dbc83fbd3694f67a60edb7f99eeb37ed9de94987135452117e3ae3258c1a68db297d79320dcc93486e3d777cf012a6773bd77d65188179f318df85cb565903c93b1256cc41d1c83f82ac9b085c9840a0903fc95e8d4a512ddc213668d7aae2353738ae4d52f782d7f530cf7e4baf995eb2a88c4a334a10714d183dd2ce06a1ef19f4b61ccc7d0f1c38455cd97d9e9abd266fb43082a54ce371bb6dfd99dc35f11fe5b5051fea00361e629784d6ab1a139fd3b0d81db49c05666328c5a065b3ebe9c927b1ad99ab8e371d7e0f4bbc1318de6a32c7f1e04ad667b877018d95554d99403312b0fe22886285bbe0f44d4af4cdc6cd703dedb9adc30f4471d4352f2ffe926f8aca358ed4a2a9b1016ffbcc8b95afc6c23a8d061aab055645a54d185516f5953d349653606842df6a545da45734959e2a48e772a6d0a4423f0980b9dfbcd10334f5e72b37f0227d956fbc04247e5b3a5cf17cd364305e7efca914941664aeb9f6445155d6e13b08521bb7c7e6b5c8ea871653c567e8829407a02f0019f09708a779dc15fac3e584e171fea0da595d350a396d122b09a7956ae277ac46243df76f02c86cdd63bd28367c1d3db5eaa48f4aa50b744f5a91e9eacbd06e46ddc85179df5d2f898f9d8cdd09fc6115c2a15660062d751bb17754baf374c57b223e89c665bc95b3adb976e75228a41f59617f7ac1e2ab9b64c607d6994a57f930e6de4a6af091f49f6e81991f0eb5c0e3c89e816dfe4f66815a907a196dbb4406ba7d4d47cf6a93bbd93b5aae815c7c59e81ca06e620946b81dff75a2794547a57c5bc1ecd310e81bd370b4d5dad4a4cf148860edad45ec61aeb4f9c12d09317753ea3b7ca8adbbe9341cf31eff23f017923335cbae4fc5293884ec886a844945567404f3e2ca618cd427ddbff4d5057e65514aa97b62fbeb6c248170ea03b00451963c4b5a4ad50b1fd0665a74472e2d7f3d7a63462ab4bef5b999f91834141b98eab42faf26db5ad0ae46e1c3f9c9bcee541fdc6a12d1485935266af1d83beb66f01d49360c995b5acd3f7da12cbe659023c88fe5ca0866527c91ec382715ee658456c5e5ebcef337914247d0fd95b4ad0d457e12dba618d2975b76c851b39eb5141d0db82db619a8feec2032fa73485fbc0e907bda5d7bef6425eaf244d5b98431ad80b04a2df0dbc98d3bcf844241f3a69637ee32e5855106eec8a80ee68ea2365c7849efaa056b17c74d3d2bbc5c216b7791c091cf0847d1ef52cae2141027aa9bbe9e6a9e21e471ec9509f75f5a0f536678bf655595426d363c901e4856007e1766a54771cf629a4aee6c45ed124acf36df0b1670101eb0c851c7c29260dbae06e907b9a37f4f00f24e03e2fa448ead62db2654f77e79989f0ccc71c6f46131a79145314bf73ba5a90c4c6016f12d22a40eab5a070346ce64dd83bfb76e12c5a42340bb0e3efc2a5c9865aa489236bc4b9a784810530fd816f77dad637f49cacf867d4ad4bb29ec9d47bd89c33db857095744bedc2015302dba60a0386a75482684050731bfd32818b9d8b6064535edcc0366cb08d1c6a7df70ccc84d0ff61309fe9333f45930ceb01a6462bf541074af8a3c42fa082962371413431e132a42696b3ba03ecc36eb2da91c34f34861ad41027601decb35be96dde9b307185f1e183d0a4b0c313c1448ace10e93431803f41888f5529590dfe92f32b4f648286b91232f3a5655cc10a2d788f5dc1ebbf23b549dc1a935dbfbf60a6d19f5536e9fa0190a8d93962048315c9ac9c7519e0557481f5d92ce5b55de5db5da21424e30b9059a119c91ea60e8a44aa82767763bc6beea7df4bfa74e5d4bb466887392010ebe72ec747a783b7f6a11aedba197d1d621673a575582c66f46d1a0508ad12b9a5beb7ff55f4e10b64f24b64e4636092d2c9476faa2f0909d6683653912849896872a3a0661b3e391b2624ef7ad4de735188671124ac7ffab3f9cc4d79a4d90c32cb1557e817045c7fb9c7f8c103a37b7334b1ec855dc65bfd42c3c367445258e49ddb63badb7d5461bc6a016b2fe9dc574569a105b069b89cdd2ca66291b9da6f7bc53b161fc8ca43c659c3a647b21ab7568b6b90aa29a404d5cc6035ac90360e57468972fe9fe5e4454ae131cc2530a48ede832e712bc1d0fef18ba09acd9234d2595e23e2978c26475422c5542f7564148bf4c8aed5615794f622d80f9d05101ed3c5dde3d340727f380316d9cb60edb39ec9c057414d75186db01d54cae222370d119f1512574fddeba8079c9b53b880b7f680bdfc9ab4097ac98c134ded72a7f3f89b7c706259f2a3642ba45c7b9ae8d94e8f49d815f850f39b01b4e01cce5e2c52374d9dd24528c5453857951c53937051d6618148d5b052fcfb670b9631e11943a4ff7313769409016e802994c50f0ca49cd1a687c93fc79a13cabad01dd643a8bcacf2cb2916e7aacecc30d3d8b7d5eead84d59df3520f7b608629be8b68371de50173de056557c915a43022f1105d459caeebe788051e4b2aba3c2591f90d549f43440bcee65a0736b94f9818bb304e9e3be48d71e9783a1dbf5798806e2f28c056a3ecb71cf0d13c48815a696c422bfb5f341c9cc8ed08b1297b4a4f400d801f72da31c446a340723f5812685dee2a6e8fe999ef22030bfa129083c4a3a8b2368a828bb38acc95fc282165173d9e57d1ca56b490fc211ee1e058a564ca5185fdc649ef3d5818caf63690f3106195763e554b76687ac275f3bdda5a806fb7d49efa9d04048c08b29d6b86581c8a2936c1082ce5226cc7b0bc890a8af3b640228d3b5ef3b35d2aa1bd94d580791e2a88ed6cf3260628653998f4b05b6c5390c064cf53487fb9909cc808f87a251b4c8da1775f701c75f9f1de81c20dff27b58fbf7ead2b729d29754b780263916fde7f75b438acccb4a3274326c18fd438e8c95bc781b1b265df711a726e3d04639fbb667e0aeb7b889d67e9add89f05382d0cd8770eeddca7c90235f03e4ca728f9320e52ed910279f3d1e2ac7b945b7e0feeb6ed23655fcb3251a33e3ae2bc503d341f9a1b8f8d64b79c204b0b9de1fa39e4c1f70e9edc3ac0209e3edc89c85713f568554c3e9542eecd55f2344028539575bb64aa83494c007aeeef285022e6c279cbb23bdbaa59ad864d70d15515ee66aa46ae3aad43756ffdd8deab1830a079319546bb739aec0a62a8a5b6aaff74df65a3fd5c85bfcf551e7c3edaba78111bcc326ec35e4f152d18602f86d17edd72f7ec810f3e56a51d0e39e3c5a548785261e4e6b7705bce9bdb09a2d57c8d9c1077458a63dde611a3b447ebd9a39e16247d20a6f48f3655e31cb05f740cb395050823a5516c68b5fc831e480ee04a46003f0b262846c4319fb7d835218a4d4d35606d1bfa66fd2e9924ec58ee677fcf24d428b6d1e50ed7d46d1f2fb36cedbb5f53923fd4826d250f774185f24fa25fa65fabb7ad607b4957745ca0e36f2739e77eeddc952d5d4dc9e1f5f5fe6bfaf038c507df649d57273190ae3977ee8645985e9c89d09a4d5a7e3f93b20e5596958b80d556e1c5fc938acb5aa79f5afa67602c330fcc60806c33aff42989a61194661c4fa6fcf3b36adff7668c49009f1be00f303b795d06770821532d519d243d27c9388cd42a1153a07691ec65135c7411fcd01191c7564a0319808643c9437eff6b8492750e25a42ef5c83bf6ff8a2e16c71cbcbcb1c44afb2a2c13ef5821f052b0885a840ef5b94554e93adef1ac0e9cdfbc72522168e34acc53c4f7a7586e5d5dd0ccb778e8509d0b73f0dd703c8b236cd327f56f0dd1d0fa0f4d6a5519648fa13eacfd4463e5551d5cb69fd6eff433f171dcab57c277e00364ca63f40cd4ba0a0be4ab001b0068e95a0fcaa48993d2183705d980df623a1bd1cf5c0007ac6da3f857f001bcdaa68c34de648b5c4494439a8739ec67e47df9fb2d942021918b9634f7e6b7462eeaa1904681f18081a42f1f9c8af74841f4e51be1b3445e485ba6cfe24b842d27e81a914c7f60cd9be9e74b7ddd230b5991fba23acedf367945fcd135eeaed824e0426a78c4966dafb8ab44dd4e9f25ddca6c50d1b87853cd2fbb204f37d65d5d4118ae8eed35f295dffd1ea2eea3678b6ea10bdfb110e613715733c8366a0610ea89c903ff6f8ac0e2d0ad4537864e2494eca61e5a4baf5344a7112e9a8ae5d81e2a00f0302cb4ee3b44a9ded26f1bed4330cc1185a21fd756b0627e70ea8fcfe11853f0e4318c8e575128d7b7552ce1ca021c398e5c97ff30fd43191c7cf5d5ea64a6ae81608bbaa822341bedc71983b70832a4fa07058fda7783e7b07eccc14d2e608cdae9833d78a98c8d429437399562b52aad769cd7a15b0a3b2ee82a54f4e0ea18ced65a361409120c1344ad3474a23d4871be982eb3b0f92cc7d37d58ee7795a2a8aaaaae09061ec7d3b3ecffb7a56c5d4f332d7464104ee741e5cf89bbc89fdd0c58d4002520b20b3eac492f9765e6acf6bc7cebe8de4e518c9c0bce6b445c7afa370649ecee28ce4f51ac9c0b2a7930c4ac03b45e592afc90623f9e66f34fa3ded2cf9cc9fce1e02ef0905649ffb1f69e5dd484f265fe3f724996dea5f179233c01ef4d0f97ad2567c634dd7f8cb0c0095da956c09014f7672e66677cf234dfc216d73ed14ce8e7699abbe3dc8ba87935fdd9918908b257a711a0c369c0c7e92796ef78e75226cda8ce22ea9d318ec588fef20053bfe195b24f4fa0a777cb6e2ef28fea27d26e1ca0a48f8eb742b23bf61ca3d0d153cfd1e3d4d183d9d1b3d6d1c3dbd193d2d1c73fcd6e233570a7dd2d413f99a4572bf1e3e3d859cf681ffd4e1569681d3b683248637ab7e750d037e61ac83902607910667500df25c798bf76cd05d9bda140d73eccc53b0c6fbd0a473fb84e0714dfc7a6ed76f393730246b4cca887519e3af7e3f9d3921584979de43cc97f82e01f50512eb164d84341506198f8e6677ff9b5fdb39b2f0bbca488c5fff66f0704aeae41ccd53b6f6f5784f830fc7111605c1dce8ce98db9bcee06abbaade23961ba6f8ebd206df73c0444c787ecd13b523cbde5600ff696550d488ca501a3c3e5cbcd049bd129fd277e7fb02cbd2051f5edf7692dc816549942802accc3af47ddc33807150c0ec3cc8df2fe308d0c71d61d3f0bf7cd43594a8f7a4a645c3b8e56e47124d1d2f9317de21adf151a320ee53cb8c69bf71ec4d2793931e674bee0abd8361c4189aaa886198fffd3fff51cddb981dffe3dfefb9aafefd7ffec7bffdbffff67f57f374fcaf2acbcb7ffc3ffff68f7ffce7d3d80eef3ffff1efeefc9b8ff9dfffe3bf26f6e31dca7ffe63fac332fc377a976ddd1cfffc0709417fb07dcbfff98f731bfe47911dd93fb36519da3c3bda7902ffb57aaeaaffc89b6cdbcbe37f9f47f5bfa8fff8657b4960ff5701d192fb3774310c239537c31a7f87e7ebfc0963ff871f2bb9836bfe19ac7f1fef3fff79eac5792814821b247f6aaa3f7192d1fff0f00acf32f2eec2ac1dfc595afd015b8a6198a89da18fcd693980438e61186dffb3c9fd33af9c7dc646524ffbfaa7300c73fde50bff6ae59a67fd8e04819bfc0f3dfb27de4af61f25446df16dcd76c5e4fc01cdbffa843f43561140212690f44e11886118f48f1276ff636a0cfd8303a61411bfbf7cc4dfc3ff7506ff27aeb3e6f49387916118fb8f3eff481846c1144c9ddc3b6a862472184624fe8864ccbf9b124bc08f25939a21711826deee7f25090cc3a80cddb3253dff62176318b644ff80d31f7bd6fa080a088ecf952002c348e21f90fdfeec2a6a47e635ef1789d81f7dc9cbfc379fc2280a933d6712c103c3b0dd5fe5d91f95e6ae9dadf093d92b471d8691fc3f204bfdd9758269a447402eabc31fbef4fc2b5afe339f896b6c37079a46eac830ecf4d77f4cf2d7eb055dc450fd5f2910c37c3cd27e7f8fc60b142bfc65614c1bd3c4f43f4b61b6c618b3d59dc44924b54e22b65f19bdcd5ab64fd7a62999b4b7910263d3847d7d8361440c606de15fa9d3822425ca868f8bca15509538ab78af658f4c29c0d0ab5de12249cd2d68597b668c8c4889dc2d86e86f9c602ce4cfef7c2307b5d953607fcba0ae1dd4326806fee9a34bc91ec7c9267389b8c3d54ac30f1876e3c21fbf76323d00f450399b9eaac23783393c91df40d2d754a00051daaea6789ab60f0bada0328aea3034267442eff1c8fb360ae07d15bdf86d6628400525e15ba6a618aedeea642b9e3447f83e98a9c6b4c9f44805be02b99ddfb781d34725c5bf851ee4a112a8945d7abf551e054f1de8bdefc046f0725650f781c31a507cf6a543f806f7faa5bfa4371e10af2555e71dbd01bc637db0f834d647f95399c6869b5fe117b3570638a0feaea1375d2501f96f161c8bcd2971c9a5aade4a9ec66cdbf9c60635790eb2ad462e3a0bb248072bf8059286973697339f7ee91c625d8a102812df68a77f2c0e2b5e76641903f78397f88bdacb9c714894d2c7e3b8deef56bca66981fa55a67e141c862250be88eb22de5a65b1d78657a294a729f75bdc1d444106b0e44d4bbd243573b8368d0e0f9d35c32c8d95458762151af4e21bc64def2f918e80f97e33e32381919608935f378a610c932ff373af33abad33bbac33fb5b67162f670ceb287e861cce84e5c418a705e92e0f75b8cbcf6533be65d0db5a5e04cfcb30129cd66da04cbd4faf1ec963b354782d065385b1cc0821e0ad8054ddcd64a7cef90565bc517de12b1990d91f572ab45cc77ddf85e9b90fb1677a9e38b2bc18b7c12409d330426ff4ab40a56f57a5b02558a26fd2dc6931082af537888ff406f97286726956a0370cd8681b07cd9eb091168f7722a7314f33b96e655776e77a102676e229d2dbf4bb2aacb68aca80ac517a4955638b371321c4d2b44166f7e04f844da42ffd5a89c6ac689a0e497ee32feacae64c8fcc24247c84283940cee7e78c539a05d0a27ad1a06a1eb4a93b364583b1f2e9e613482f9d2f2170da2a686b3ff80c193a2c183b8b85e0238e8d4139f7a3a7fa69ba505e490a34f2d37475341f0af1819fc6006961a3c13558a05a68293f1cf2386c9210fd16e908daef47d9fa41cd1fe52cf8306dd07ce1494135fb92c659e4fd86c0d3c2e08da4253dc9c05bd0c0238c3cc1310422aa4f632d01a5595c1a8543e2721c0ba7462e050d59225da75cbfba936314eedfded6478b3e29aee7dffdc66b1295468b43b43e25e35934291ea9d3289ca54b92b230535c0729b6059b05a93df0eedb499a20f254f467f4bd48d9b8e4cf3666eac76c8d71941d0a953a7aedf4d7d549986703aad9dc4c6bddbbdad94c651dbcdad64c133c7971a7fb3da58974776e9d2d47cab6f56844a4c67e9a471d9cd9d06f95cea38361233a370f47a63858a8af3b850058d4b75b1c58ab542354939e7a5da2fc2c0a11a78a21011edd8d293bba8fd35d92253e35c1376e604b2b3223170ad83cde07636cd64a3de2302cfaeedbb3f36843188bcf7b257179eacbf77b34dbbb35b2d5e31c2ede0e5aff1c64eddeccb6de1393f508233f994da8be6c906ef94442244cd98867772682276acacef5c54cc8ec51d2633112c0a33a558f0ce52e280f1787e52ba9f261401bd50cf79e3bcb9a0d3716e821e3c2a6c6fc644a9d7fd563733f493e35348773aafb5e3b4d31c444eda724047d4ae34217c845b90a3d4435db9b29ad4356db7a2c02d6ff7f3b71b188dfe58bfb89228a19bfb939cbfd66ecbe7920762678a022a2f32a466933463b32e1b3876717be6fcbe8e393e8d7133ca29d3b8d81c903a7b499d3edeb572554b1e6f3d24813be7fa8bf2ea173f0d785ec85b40c7c15c50767727311725ec66458a8f5b64cad42feb0d901df94a7071c0d29e907c8343475a85ffe2e7c2cc6661ddb98987292cc1bfba09951d24a55fb09cb79eda67db9e0c44f456c17a23a2e0f5bfe44ea5628a249338cbca7b61c2125793f2b734bd5e43ee133ad3c0f8c81dcacb74c02206d16e097283a3c5f539fed54919b3b424be3950f31b184ccf89d0348858739e7eb7d1522b9800a906615be79b42b1389bd02b50d0faac34e0022fff015fd94d8e27f68d948d5c84793608d2441f3fe50593a99d84578243fff332fe2a28891bce45f8076490924bc5f13d687063441540e8d6e27f2b9bbc10b943e6e9aa494f330fd10b7233f8b3f9351fabc9817afd1a8ee46b372cb5ef0aacaafb3a86292b96b501263e920f8c645cbc90162005d4f8c083a80f42bbf32d06892fbe7683e66ded6c97a08baddf60000c9354bf05f9978d7a14c205559b41fe3360a8efb2b0dc0d680c481e3bf18be3139c1f704ab6801063d02a24dcd0de0bb700885070b9d7d88a2e4a769e8becbcdb7981fdbe1d21b9fd8ca72665237c7e22340286d7e329d6e4de03a02d67f92abc4d5f4faaa94080b9baa1a12268a48bba16ebfd04bc1c021e6bd237be3ea01310510d03226c8572d32e88782aec50fd66ba55037ee5d21238ae5efb664b8c462248ad736aaf5d4ac8d88f66f5dc336e9c1044297f41cbe4b311fe877ff26d26f260b894f49359658552a6cfcbbaededffd1295c3b907dcff7655ca5ba0f06fb87c5283943990cb9bb0cd7443f7f018db8387648caea571d43ce37935a1cea634817cd8df7be8b9f15a39750d9f941362a11deb273d9386c4940d3b748df9d7ef9755c637595f87a7d5cbac302138d71692829382487c9ad872224d28a5495b021e5325a064741d1dfef99ea33bc0419ac8c64fd8cf601eda49dea58f4e97b28d6f5b7abf61af272c9438ea40187779bcd4f96064fd891a1a45c147447d06640830fa8ada122b2054fd2b0ea2f9102327e458bfdfd375efd9dcb566f0dc7e98a7dd3a6a0ab1aca7d5664bd79c33265ec85ef4e7ad9be1794a73bcd0b33940caee3da4e0eebbf6d3fc55d228a21b94028363d9e1d306a9b6c158d7cc1d3cb2b5efa8255a8ecb967a6efff0c167c617f9919c3e4c782cb5221c952dc525937027d4feb00684bacf643ea7d9a62eea3a66e3ba8c7d3666ebb88cb3156ea168066fc087fc3a1a8998152e04c0182ab350d477b9d2f84a5853d9f26090dc4491e1fda28c24e62b8595f8013c8474af00f43692458930ce58fa743764312860102e94662ffb8e33f88ca0bd8e8b9f7a45200302d62364a5b97489e4d689c5b691a1df9d228b4f0e4d8d20ace74f991b48d82eaf86474a585552e83b4ad26b279a9b5306ab3b1d15235070566eb38fbbad20c3b58e4b2ca7141961a8aa87aa89324fd8d6e341196153a1997b46e305b8b639b8b605ac918c5a68eefbeda3d4f2f37b4f4453d384c548615979a1c7486983eba3105f7e8dc4f3615d703cc7868939f74a6fe4aa16d609c27690c0121cc6b3f52eb12f6733497b488a92ec4d9d495af26c8a9c1d8ea8c937b7259287317c16f7a5a00e3626142e5509cac944c96d5c2383fbb2009ab2d0acdcb928f7d61abe7ad786bd5a05175f23435e212fc4372f3204df7e02eb31e36d319f4bb96cd3cf51f21815c3e0d19e78cda4dd9670bc2d2f290a5e880c13847c0afdcbdf3f372936fe3636e6ee561d4e5bb60e85d2e9cc2a159a2e19d99a9104a5347886f601c6f909e52b3a0255a0345840a9c8b649b559b751dd3cd58dda9828a1c476530db8c926d628c38db5ceb03dc37ce8bfecb1753ab3b5e4a412dbb018733a4f7df53bcf95ccc1123cb3f0228b387fb4bf302f53e9fd8c9bc410cfe4ca7570b0ae206a4e80b35e209a4e08b25e2b964982b4fdb956e8fb8120669acc3ac489d83112e3861fc1d433c7350a6386bf7b88bfbb093eaa63001bdcc0b7027990ba400b243b40be883b9fe4e601728b67c1ba20e5f3a1765b66db3a3ff8f73b659284db2b486115ea19214114262585befeea1091969963eb240abb14e17becfc357869c710eb78622384c2933b83aa457d1af903ce06c190ba6168c681e97bf021871058dd26af20861f7ce6c741fb963dc7cf4277d97d8b498473b4b8695b47a82afe3eaa36b81a537967b739e5d5ba79345b0c507c499d7f48b72295f6d9832eb674718d9fb876d10613e46f97c96e69d1df3192d519d9d734a1a4942c3e44ea67e861eb270fd9b4a38e959e4da308cd7cebfb793c59b344e408ebaf53bed4795dea17411fdf007871a1c8e4ee68f109b9b56f376e491dfe52c93a92451a75c7af9cb6a9639fe00220c2797c9c227e37caef252b3ac11738cccd40dbbc2e7faf7bf859dc19fe63bc9d791cde644c87411dbe61ac9d216f1e63dc9d591da161028ce56f2166dc9b551c61677c86356f51601c87d96f5e65548a69188e61c49c8118de63044714e22e8b4389f48783154cc8e46f8b7dccbab68a39d8f1d179a6b3619d498013b84df953c49d663151cd6d518195218e751934db9eb19e335719efa411a186723aac113a736b3505b2bef3cb292235a04bebb30525a2cb0e80bcef0dbd60770801035c397e064313fe4660887128861c8dcf481b0b0c8098ce2e973bf0c7ca5801fd7b7b5b0a20b20e576801491764560900ef8afaf6e4e07d9f361d822bfb86b5b25f1e9c3cf94ce6b9ef3630eb211629b7a179c9722aa74da41eb8e5108e6cfaae472ef47b79e391f1d7779900390e56dd009893430a15020fe7f5651390516d5fb075fc3b89d5420cfd23ff3064178b7db3e74431d62f1164c430a3d2b94bbea646d1d450d59877de959164bd048e957c999971bcf85470751920a8bd677b54afbbba660ba46791e35b63109fb4415b3dcc5f4415f7d5a4b221a2d733e8d6d529cd312742a490493b325ce072ee10e34db3d997774bb8a8931fc405b5153f1d0a149cee9f88e0500319c27c27469cbc0008fdbd2fb692e136592da4a23f6259575359beea28bf8d94191c2b39ca64134afa7de00578b34cd8dc7b9a526db1824998e806ce5bbcb6da49891ff1aab405eee1fcaf2b4ae7cf8b5533cc05820c4363c816d12cfa04619530a0856d107dfec60537acbaef0dc1f09a8118bc12a03da65882c171342ae0568d99c5d599c3e9b98cbd5d574acd02f181c347feb50d6226b308174efc520bfc91e8e97b4e8b5d208856e53e5248325e72659d6559563b39941a863ea0f02da294dfea88734b780f29daa59d0c3e58b3f617195a1e79e7c6b83013b66379f1ecc454c7568edd39d0e5437d72fbd5a0d35b867d774265aacc144641e141b598f9cbd7fd57b5c2e539cb84c14f1ebff889ff9a3774f90d9bb2527b4bed8495e6791cd2e6a2839fa8c025fc28f5b4283fa2345e0b82d35f22d5e8558c94c7df027d1f8ae739566445f04a4e20814171dff9618e582f51075c7145bf187821520ffef00b7e5e08efdc2b5eade14fe4a6eda97872004bcac412c87e131fdb3f8555f4c64ce1d449c8f730c09cd99c9d5ac9dc2fde044ac9d2afb89a9cd2ea3158887b67808316ecd0df57fc38612aef9cdab5c5987eada1a4b1b9feed319ae9e6a2830905b02e7d5bc018dee798c7a271f355c971893cc31286a48c15118f0530c49d0ac64e5ad4b094df5e1f7c90861f5cc484838c6993898ad3e84b882993fa94607f325b4fc763a3014a3f944bca3efac21b37f4c1ab6030062457b7201ef8af2a990f5ecaecee05bd0c5403dcb7fc0dda7aa29be364ab5108b7d3d56f85c75416320edf584362fea6731800304df4c13c4b02f9d1b45b5201af5047fc4a88533190498d2083d8db2f1a238069096d84d912022ef13c0c80636c9077d04fa7c9bcaa003e6f80aeba375a9f2f4c25d366435f5f3f7b0c0603fce8402938ab16876890e19293707bdf4d117fc4c51c0516d4b76f3e5ab86bd7302e5f5d5b98028105b9e9c382c786c5d899545b298caa00970bac80069c416a4c5fabc8aedba6f95db6012c6fabd7fe4daf9b0254c323834f215941839983b4c827cb609d0f8bd3a1862785a36666ee1537db5904d4bc0a84ae2009f78e69a0ea24d47a7d3460dd3ae25bb7891c798ddaed9d46f7acbce620061dcdce0df89bdbc228616775a62d501c28574b068277b03c76bdb73c1e7b14849bb2a67bdaae476f9394cdd09192fe14dea386c6785377b3c2725e792608c8a70cab9f9c7017765f786d8a6a01ff706e93962a0e8d902f3dc7d11b3a0f24402b795b88247b640c3174c35ea9fb6bf40b049e3d5b863f8fccedaca1fdf8c8a953fa440a34dbeaa0c02db73d5c8e68da29f0a42c915ec9ad897729a4a06de8f94848bef619f4a92b268717ba585d85b0c2acc0914ba88a766140af6a1212ec76e9bc7c9088383ea33c66b2f826e3d19a5834bf84173587238ec347289a9cff4dab7004629a695fac2192470f968e84b11c8aede02ca86e49ed081d4f19ba55beee8b3803190780d92e17872dcd02bac44e812cb2a22a762348479c7f06e3ae73efff6e753d6b9e4f9c5e1b09bd603683c8a454ee4c775f82c01b5d3f640dd1acb01a97577d76dca72fb845eb70981694aa462bff18d1bcb280cadd513b46ee0fd04adbbf225a224599c613f935e8a1d811bb427bfcb850b97ce22a8eafe7878a498cc6640c630909c2e58397303981b586f1560dce2860e6fdaf83a0cdfad1359c80207f7e151ad9d5a720cbc6122f78a03a0d9a38b75789782e85b2c56c1a3f7a3cc093fc08c22c8bacb84e2b74fefa980b8b96712dc471a519c9851271df41ea3a6d80df71f4b600937dda8b6ee390657ea1b4a654994bf1562e88c69b4395288186c58a78fbe58e4fd11cfc42552b92211b1fdd92db68b3f5e6889a3e0367ea68a078d4e18a76ac077f518ed0accc300c120a17398c3d1b275242786dcda9a7b0623d3bfda543e4ddf292e1713205037aa08ac1ba6814dbab2caefd0b043211e94ee792ba62eaaf544cccafdf138338dcc9d9cb83d94779ec515b46f9aba5aa0a2cf61238720baa5e84f4ab235b5d8a26891d7d86afecc36fbd16a732647639f4aeea18914f521a163eacb5db75d360d623102dd3587d904458321f39d4b4dc5c8befc8f4df2d84c0eb862753a2192c9e363acee96b9c55d75adac0ea013bc5df2ec31efcbd0cacb8c6c57e5fd9eb53e20e690516d6219764be23c7d6dc10b859c9b79258c3e3ea19977435cb7184ee6808ae031ccbcf25c2aa789c1aed017209c1faa5bb6f07f42b5890711c0271e3212b27794e4099a772c6be2f8f323ab9103289f386dafcb6152b4951a3aeea8036a8763cd0e4cff3cff6ab5ae84bab2cb5e03a251b543de8146174e2be439425637de0dcd5d99eaff35ca1c93407e7ed9f97f4d7fe580bf5035b158f8a43b12f72aac7bd347923b64133f2bce2025606b02014cf1ce74405e6541ba3fd24d49bc676a7ef62c43038d1c75598f3229983363604d851a71dffc95c52c6aece73c18384ed8924775f042bfc4ead2a6170f2aba21cbdd32fd9e263c634a299a68a6c70997a8c19c45ef1394630e96228e9f78120bdeedc3e4f460dbed60e0eedad134321231527491bf6c33332e4ed42c6415c2aeac7810f23c40852dc5dd3267fc4c747b0891a524b9cc97c2f43188481f2d4afe66668f850be4b21818c6e0a20e8e2767de052b083dcd506c75e94b8751ad8556330bd134ea8d4a8da88868b4c0705950cf0c29ef3c3768cda58643d87cd677c1fad7586bb5f2a53e9a20ea05798ba58a1d8f803e6bb44b30a5d4521cfaa06bd31941a48166056ac9acb5dee755e1ea131b0ec11e281153ca95f80cf20baea10b30fdecea453cd88533662d78fb284727c45514845839b03bf51249febaf90d4707fa719083b1a84a6c6d9329ed2cd1ca1ffd0d2e6400aafafb70c34d8c160c32470dceb501cfea6e6d3c6ea5ba43cabecc92f936bfc8e6ddc195b6548af9bfc881fcf2ad3e9954466d90baab6ed40f865cd1eb40959a63b1daee1b2d1990087eefa2c15ef6d91521973a906d644dbb6ede670615b1825570271668da15688491083704e466cc0016745a111b43fbe4172a905ec675a2a263c29fbd48a8f22478cfb2454c26b19f354e5f7747dcfee9fc6ca5eed748be7e9c2e7e32bc4b7ac1ac3e3657b38664969c8691cd4f8072ccdc2944958022d4eb843c0dae9bfe20d772e877a9969e23ec36a306bcad3f233924c292aaf5abe5880ea1c71ded6d943987c157a2a65791280a0377b7ae0bb2cd0ec776d716b815f7cde24e98ac49373a0093764a630d53ba2f5bcc70a78b7f70018b3c2d15ca614412573298d19a071ff0e359948fdd7f2222716edbeffbc2b69a82a60765eeb02a56465329f121824925fb1aa09ab1cc358f3980de99c9a8b821300f77b3131fda15e3e57a8eeb921bd44ada0256a4063c683b105db4df984857197be208817e4c1019539f94dbb57bf74edc1b8ea0f2384018468b7632457fb7041053fc54f6f40904a735dc5dec25e6510ba90cfbed05f2754bf43cc4ed0bc501567ed0ce18ee56c109063ce5f2f588e9d5a88754758edfdac6ccb7e5c63afa278594e16d91ae6722a87351961b27920be9d98a40227b6ef202c47467598cb566c59530c73ddc3c7fbaf24f21fb057e10f648dd69d3afcf5a66e41758520c3d85b1372baa91acd31f9b41d298a25cc10295a833a68f3e21c5d7e8733dc5db21f0f432adcd01d9be5583784f04f4d12cbe2885da8500504ed59b175aed3f70b134e6fb0f393a068f5cfe5d0bb7578126daefb841ab6aaa3d443b57dc080c23ec1f5f959648a4389f6f38e8a106f7a53c1497cf0ca3ccc98c6157299ef1cb0e11501d66fee9e18073dfe286664ca7f36bcc3204233cfd333c0c32faa472a62ccf9e7afaee69c62082e109048bbbcaffb40566348c33b62a8c5eddaed0fa10c7aa132d99da16b8171f33a9d733a50dd956ffb30d25b6fc3788edb5522c1d23cb6748ebd120771c4d99d1490cdfb1cfb42ce37562ee4ed11f9d1f96667f915881f5dd179340af48caa32b890c210ebea01bc856a5ce3cfea101f482acda7d4f575ae6b00b4c48771f99c68e229a9eb1de20a21b167839c189beb726f9ca9d771437fd9606d00eea7c182975bac19f4b2887394dabf441bd94044d764154fdf01c0f0e4cfe755ff1c089718873c2ce9c4259dbd3f2dd0ad1915b35833303d5284f789bbad6dddc1d79b2f67ae85b6652ef50600af596e8ec282113357ab3d26d0c2d7e66cbbddaf9dd296f3828417ce2fd6a5a297a81334782774ad719fe43fde3369bd4ca205b7ad26ccfb7321ded242a6b3a22910d84bcaabcc2803c1d01a57b0af386e4ded49c76f288fe412e0d1eea9a9ca5cf0fd6a6737fb15133a595575e971195f991b9d1770ec71fc10bf43f69b831bf401e4d204f899d267ae74fdae2294a0139bfe980fe21c854a63f355619951cfc911b24f6e3cb46cbd12e6e196a861a6e6f05eb0d73a15d92d300936e7805eb080dc114729623340e4175332dc0e578727c097d36fd2fed5dc889a314be873383467870e6403b8b748c0ba8d1c29fa3841f486a1928c8409217be27807e4266916554ff07d3d241d7515a3d24dbc33dbb633048cfd13f86d081a2edba6689ee627c9a9d0e519d260a66fc7fb667755170e3aa36225b419fb39246f7951b4b1dc129ac0969b8223364e2131e4bd1b2a0b4c6716060471193ff7ce1168df783f30ba1b65a754d6fd2fe61aff78b911f08e48edee3d4be7a265b6cac765295cbf14db19d8d0a7e6f6f842c4866ce221740e6553f018a149977a34ee3b914628583d45654b22ce4e7c4c921d63ed4447989c4abc35f835762ef72e226c43368a3e281d800bca99176b384dd3c90f997206bc134f8050ce70d7eaf5995d8e960c93b33bd4b66851421e934a7eb77e399f2b99d01d78bfd6934faa534c0d87c0633e4b9a530b2155ec59685d045a6c4ce81ad3e5a8236c63e2407f7bb0ee533559cc10c86d42ed567ecde6537e09f736f451242cf3b4ce2ca2296c9d27e5aa2552dcdf72f8b566b973f31a408402648c7fe097083fafc2a085895bb6b5bdb9b4d71cdb053891269fa95bbfdba6d90e1443321435264397079cc45fd580177a4100744ebc7e057a97de5755a0d70432964894bb3471277fc96d0497c0c51d38541ce3d7ce20e89cc787bc3992c9cf9b18b65f0e305e3f57bc5a720d639d29106341c4e6c48631039e9e879feef7bcb764c398101b615608da11bea535446d856d9e4887ae52c22a1a80de32f8d1b9b07426c9a1e996e5e9f798da3f12cf3e5a0ad1de52f2d9f81b6568939c0c4314d2fc44ecf17bcb0ba10ee1572dc2d4a0df10556d80842d68368b42285d2171fc4a31dcec2eccde59c0445f185c38cd3d353782bace50ef4a1c85890798b38f62d641b5d059d62e66113b270e7cd305d36f7c775e7ba49c85b837f4c58a3cc7ae36414194883ee14ac802006c7bcd2eb62749f45348127de828faa568a623e34cf8f073f45b181f7e6e672578eee8a1a4b4ba3cbc41b7e449194fd1231124034cb55f1e7d985f466e9a1f8a45e915585bd9b6fdb1f4d292552ef9d371392af8abbaeb353809a5b9f7aa5059791910e983a1c7bd2b3bbaed704a249b8fef584ab454bf136b96e3943bc4a82c976a32fbfdf2e7817adf9190ecd6c4e5cbabdaf839cd182cf52eab75b0db1603d8a9060722dc2ad1ebd10712844c9d78c20e181a37e654e3f8c42ea74b5aa19b546d7a5d6b5e2e054b9641d5ffc7a159235b0a0000ec4014b895380f77eb7077e7f43b7fef9099a4081ef18ccf9a98040930b864b8dfeb87822ccdd9abca7c12331e739c50b7582937fe7b13c6bb3a0ecc7ecc38937542d15b13d9c92abc83bd0019cb3ec359eb7ae0bf411fddfb1e380cb8eba5df33cf3538eff56b253befda0bb2994fc402c74b5c6125d459cb3785f7ed2e3895c294d1872ed60bbc03c32ed3a3122a92aa9a28201e2080b5b29222803cd5fd814b1a7845303d530cc6c26e9eb644325e708644795aa3a7a6fa7d5f3b8261f3ea7061fa97b1e4caca4d6e78a2ec2800bef16db3bf632a8f5c11af325bdf410ad90613ac61abac3cb00b3cb5436392b229cb7eeea8fbe6d0ebd03d479c687b6884b14b9d710a0714cf4ce62efa59936bed10993634e8c2284a05173a91dbe7656e78c426f315d442a202f95ca8764e402367048ca15175f1c3bde855445e080b40d019afa749908a423263b1f0bc5cd934c1b3a1c231776f7fb759ffa0eb2734b16cda6ea85a35aca633e08a68c21aa0f6ccda21c302acc19820ba40d594a4435d5976f0654c822db904fe42bafc862604841e4384f6f06fe86a0c076bd411a45fe249d6fc7e8379308c0487905773f1f8c84fb87f4c08b14bad7906dca96427b72dca55925ca8b7cb30a34ce377d690d5a6545645d4e05dbad588d217632147388dd627e270c6e7049c1760963f63912ad6a2738fc6c0d7efa3ed2a1d3ff4665bd1a95e1fe01b00c1228ff68127e99fc374bbef761bf2adb28c55b1876d445b7263cfdd0fb9e655a1117c72186cbe1bec4f55418149329dd99531e5b1ddb1b5e4718d1eda4f4f6b9648d185cf4c15326e64715d7725888b51a8ea1a2ced5db1845cf0977601f303a4f008b57a0fb1aa25b60078baf6fd26734be0e825a24181d4ae332ee46ba7712bd2f8eb8153e7ba4e1aa50f8bc089caa8f00632a47a160b0e268844e33dd3939a2e1291d76f67d58ef829bfd047d8a3c3fe6c810e22b3de26d9e82fb0b0c5ed0303f1857fbabec6e05753948e3b5584dea475de7b59ce1d4101926ff8e6248fd4751be8ed405b49b9a42badbedd642184a5decd824fc6538a9ac4c04f5cee8d488b9850e1be6c77ae677fa91424472e943a81f7cae005f94ce8324fb0522509cbead46056d93b346d01fe9c20ecb96f0612bb98e4da857f1f6a6dc0de50bad944ecfea0f306eb449828d06f9ebf33afb0d69cdf4c76825d0f634a5136d7a8d5fa48f5411b95489042580107b05868a44551068542922b2b38a061b2360194b0d24041ae171093be9595dc0e2073e97573f7c5771fb9448884bede6ddb50aadf3b4c257ebff05142cdbeed7ab5d29e96cefcd6c74b8e0da84e89a2acfd0aa7069c594d98b10521bf0d8e4453f8dfed53846678b3c5b7e293effcd29ad3ca48dd9739f7fae31f3eb7c5d44e4a3b243328cfca2843161f67f46f8a1b7cee7b948290a212bb2be2e93254a6fe9a00b27251371d875f9dd22f12ccd4aaabcdaceb970af9edefb7b7aa192636b2aabcbd303f47aa987ce4990bead4b324b9db33f8c6a92cba74a008e0fa801149c436d24db34e0eac0b6024cd827382e1fec8e04aec30ec0d7d0a45ace230855f60e209d104c74e331248a393af2fb5ae1f3ef44c3e52e4431ee36d91470a5e0a1e90c708664e95e53b025958029ef42d2f00e879c866c9e5f4d66010250ebad0a0315c164a06347e56d5d5aa2eb5211e3095b4acd37401737836578af219772a8d4729973ce7a1cb6c9d5e703f02c4e48e4042335ea37ab908ab85ef4ef153e094179552c8000627c1a19daa540b5294b21f6ad9e0f673abc5f5d50f2c4af343813a03602af8f003147eefdd1d91ee023d0dece228e79390a4493242dc17f5919b323df7d5bf5cea62766c3b83cb8a486734ce52b46538f475c78493f0f57788439215c2437550eb17493631e171c7075b79581fe4d2abb31e9d4858034a6690a27d24d2803e67588848ceb96d40e8640ea7df7bbe90c821a10663519f028915b88db202bd43da314c365dd8eaade2ea1e8e4dde09b1b579b2e89548e8f326d9d81eb7706cd16112939ee1c7f9a0bb6d8053184a09cb240054ae0be7e765b6fea2d0044da0dc04ac455ea93cc052e2fbe45fa2a25c4533029938957dcd4e7653beb67a37a340c2bc587b8527983cfbc007539167594d3236e031188afa4482ae8ac094c8b35f7771c4d2d54c189ca51beec572ca2a1924fe43e62d51069de26a617df36379f84875efedcc1a15d3b8a5dbb8520e0dca871b3712221ac1bd3de4999e1d0668d36a2bf5ad171f687ca932a2b180132c4d0040e9627be06247bd064e8e4b293e1459ca49a60313ed257362e7af345c612cd1b0caa216f1060dddf0e759156500d322ecb299a08a0a90f84077b72b4e11d3574ee4f7e4a891275cc32f62ab72deb2b426166b82e2e8c6102b6e879ff3af4b9a5cfd9c1ab29680db4e685a5687f2a3bfc0de495410411e7831ee04099e58842c52dea4f8da2d5c5551b60da7658e8bebc47275de87f367a0666a015952b0d6275e73cd33db8839729deba85204b0bd43f5ea46fea628ab38f3e142202740e9a33bf8d1a39d23ab083174d273f2d2c08849f17a8b59358a0a64a0c1589d2e2a0f67cefa0a52b551735c485ce6e4ca35d8955eff5de88fe195b8273dfe277395589d2dd43a399377e04774e5399b9a49344752577877031a47fc16a987570d869b71e05386f89df8bcc07274ac0c0517dce39f8d07cc48fbd67509c9d37742006df3b46c6c4db8a2a1e3657692af5854b5b62de4793e4ad9317f17c7aac172d27a06e9c79de71a838dd66e268ab22a987128e78ff32f70d5e84bdf7e9a53410649537e6e6856f95dd888ec24a0f20d5f6638a0f9da2ceaa1a826a1f2cc0f39f21ebaafaa27b5fada790d97d05be0c2fe05caaf0b2343fdf2cdf38df1cbaff072184f17a3a02c03510e4be0785ceb0f98903aed28808d00cda2d85b23a6e3ebc1f08176667a873ccb9af96ba9739bb449e96b56a6541bcc7e8e0af5a006d3186b65407c81680e5b4b5532607e7d600e42d20abc0b748537320f02a7c3eb92a46beaa7af2d8935d134eef51b8594ae2f0807690785c83d72c0a67bcf96a2696bba60e7c0ce0b56c8d685208b992936a28d8c5a628217e09fd86c996d5519a90a78f1b191229705e5133f49d925fed773a6b3627ddb66c63858a34fd3f45e64132d92f90da1dc791a15f345064bf16d57e8eed52ec85cbaf6a13f945598a43bbdcc1c0e267582ababf24d6134ba03f0cf9be83cc3dc66c7471ec24ee5fdd69f10ef4ae3061cc5dfbcb7a3d99baac6e4266ab525158d629d91510ea5fccea574cd5493249653fafa47a63eaaa26f34a0df4b7720f003090e10a3815f11f9eee338c7a21413b65abfe9f4850f491375b7fd09ea6fb7a6bc6fc1e542fa4b3ea5cdb39a6bef0691f9092b7cf652f653aebec134e0ded21e222e30ecda83339895c6ea0cfada5863887012a3fc12209c9bd36c075058f76996b1427c5bb1265feef636722e15fa17b310034a8cf7acc7cf8e565c9eeff8874a074d170b1ef33c90bc1edb076f2491cabdd31e49475e7e30b62bd6865e5b03b6883c15a21af12ac63eec2c63bc3a6bfff6affdcd91af513b429f71e2a57769103ccd01c89740c9518298e4d731dd4cd5357713e052aa4f9f2ff80b2dce694987a65aa02728fe195037010d4e3070d279b7dceafd9dd02e50fbce458b47a43af16b41859029e0b21ef04bc3a56c2bea3b70924eed1dfe2ab4e767804ae1e8435b35965110dd59e856b0f4622c6eee2434ade7b39a2a1c9146849250f010d4edf1cd9ed3c070d9e9ee3750284f9c1923d5fe9c8994c9a3054c79c8269354972c43f662e42d72b8e271bb880e2fd60999281ae9a6030283a8d7702d0346bcda0144893ca3c4b48b72ce3d7262349ca9c0d0611eaaeaeacbaa244d36231b475dcceae7974794051d021f2b4116a64f3d33d913b852ec1fefcea6c4829747f26cbc367325010d3b4aa2b80c63e49353b13559c8c60f7c02f28217a4d451cd2419e400ce1ffcbb61b4296be730171f7d60f871811cf3190a97fae4cd7daa2de7d140c5479cc98db3cd5ac3050206a93fa411c686cf6931c0c09b3bc48039a91d63be5e208adabe1904a36b0e6251dd0ce6d710b724f7919f337d3ea37e55f41d5721fe381d7dfad1eb4f289680aba288c39713dca0c8f4d9ee52bf2affe228243d576228010800568580a25d8aa6533a43014c4199b4b636ea5bd7f73b94609c9bb4ad9a2733db8ee85750bdd980f51186db94379a1747fd9f42f783b9343f621107bab0046fdb2165e64eb785f758e8e883cf3cbfe2e82e4bf4f018719051f13b9fa57e954e50c3215e87d6683e802cdbf2126faeb9d96648a37bb2844d0b031600a2a17e3913b3d22319aa44f287f4e86f4b59491a32fd048855768ca987d457e31d98ec7e6344fed8ba3447c6e88cf899fadc566c95c06682396ae2ae255cb89164b7c85486186578f33e6b23653469b9ffa4721c04ae911a71430a5db4bc468dc7ba018630096d296e332fe84e0dbd260b04d3327e6c442651f6a1e7070b12e7cd010acbc01c38e918234ae425f5f83adfee4bd28f298964fe9d51fe89102e9da7cf463ac4ff006db2d98df5d70ed7b725b1d1bde6084d8951eed2fb561d6c63dfbd1d04ee98d8c508350b699b664a6b61d9ee8fe6f1381cbdb906c89274937bb366720c3d6ba78c608ee78b5734ca3cf2712a47b125c75b7322c8878e8130928927d92f4025a4eca8ac3ee25424ab44f3c0c0e40d2a6deb785a438861982e1a84c8fdb73078b8052dba9c3f1702c032bc92d2b9f19ceec6fd6651f790c094653ff368769819e3e2ed33c321f77ac9eb62e89a087f7664b7d64376f66c0cf42971f4ac9f2b51c6e3996a30f7428472748f65b2dfa2906e93a5f6f940ce8fa8abc7eba9c4b0e88249230c675447456995ca1e2adcb09dec39a3ce68a1e5a262ed7df7782927b1d5a77e36c52db221e1d34ae37b6266e31d5b4be616da0a7fec4ec1435de29b9a2477fcd1f7316aae3c363345fe70a1fbc0bb2dba8f943a028cd31ce9f95bf9d09251bcd09e61870931138512d7615fe117b85bab27cb4ef7b1bf4305bc0cc2e80a16efeb362dea0cfd18bbce62ed7852ef1d776ece9c4784f5d7281229a491a3836b746c923c8a7922daddd21d6d5aa9ae185522d711171334de5881baa5f6e78bfb9971fd7706679f29d86f25f5ac6e2397c39859a5997bb4b96209705e38e608021fca2d4a62ae88d480d70c481eb3983f92a14f3b012cdf838b278d022bf2e13ae73088ee00a9375eaf7892af336b4382d82bd18cf56ef727cc99f81d30396d1acd91f45c5713d5685b25ccd7ecd153eae50203f770ba151f37bb797f1d32057bfb6761d8303555174f8e6875aedafed658499e4f1200fc3c774adcadd95ac44642e97889c06ec088e8527b7a7baac6385b963ff65bee8223fcc929980216cf67245c58586b79cf0b9f1d51046f694da27ff9e237620bc6449273ee027ea5f73caef5210aebf5f6939f270c0017a8accdafbeeb14eb34a73afe5452f27c81e5dd007d82afdd5d9c04f2412e8b3b0cd0dc3161fc7b700125aeeb6ee3bec30f473b17726533063b2357674521509ceb9d8a6cdfe55f80c93474831021b24d979b63d83d40c3fcc66e3c11c9e823afd9f7a16a89cb8d57000756824c287b67568824a3306c204583f7bba04215a8cc907226732a8b138a305a2f198bf45017d83f04f5ae208b29aab352cd18a29db9389cc9302552753573ed8861c6f2b08589e869cb11cbc60df8a29c462782e342d88244d69ff1e70add94d33ff64b351941c28251ec97dc446ce38daafc600ca60aa433bf74d7490a298e565abc2506a7ad10005827f00fe92ead0830f0b2f04477112b32e3cca4145ddf99fd3eea6981b89d323656dd3aa576aa5448baa82a794c37e75a086534d97c804d1194f590458f51c2d92afbf5097239467ffba680008f1140bdc3d4bfa281083b57a3884c91e55a47c9be706125d841c58b7c4d6398a3d8fe00d0f4c1f1b488cf2a051f12964a42d284d6ee356f303904bf993d3f675cd080c040b0f5693ca6bbd271533ad7e0d176bc939d848fb168972f2166c4928913ff521a4f443a4113f0c418e320484abfdbac16a28fa89ec4b1b0c41a8dc2e33182632971f23cbf9d2b627e5a692c149a565cb57d04ba8ffc88a38d63c919d71c1835f1273d7028ae90c76f5fa75f61d7eea0c8d9fa28b9ff287380957d7a634a2b85c24ad572dc44135f9c8fdae22f69c3d64313005ac59e6a113c8ede58cc5e079f23979c0f9277cca556cc415242710656d5d95c34ea177067a5c760e0f8dea04743c474d3d5c9b8636f08f8e5ac177a3f5a6837edddcf62f334455a7e6861ee545b3173e55a0696b8f72658ba56ad6ef30480577f6a7b6908243e901231011d3107636efb098897d5af47919a1e58c51c76ce7a81b92ec075fba419556c1540e4e8f5294fdfbf5739d00ac5e73b7314b54d5b3e1f75bc3dff8021e33d87d3dbae5b7a21918e2f82333361c3c096bbc1768bd340b8dfaa940e8f3accc1b6215df9bb812800189a29b89b1413e604181ee0b40723555c29dc7825e54d70e833f0c4d89887b74d9cfde23efecc9de5f201ec7fbc24e02e95c7d92268d9bc40be49a711c5d24e615f206fdd24e1f0f94b094642ee36ed6ca5d704ba9a09518658fbe31f9d467cbf0713a5695ce107a9e9df9ac866c84cbc0a0af126f90cb3602713248ae63d470400e73c0a87624e28ba6da2ce3c8358ad0dc8af4851610fb0c9ba5c733f2b176bb558c09f935b32e1ba4eb8cb81b3f10074f80d0c3683b35458b5fdb0dca4d6900ad093a5d50d5c2b9cd707851416aa491943b48fdc29ecbd9393c437a51b629181ff60da82663bade1bb39660c3a10ee410a491343f7869ac954731b99b1c111e3c52f5e9d5ba3a92b02f5e0fd35a38711955744e6ef6b60e8c5031478ca1145ae12c076f5e2a1ac9bdd151c64d1c68d5b3846a124fe5901d86ebefeb3c23420dbfd9aa548ac8886c481598641993086f0c44af02e6cd1525f64fb6d422054c5ca719211d8b8ec5fc66ae83d0b4f76cf41fbe6ab737b38757f4b9804a40cfa7cbab24bf61970677ad0c88437bf426b205da43315413786a16cb41b28da18aff93bbf6c406172c93d65c261daba9013495d36ae60493f3c64ecdf3e4f7a6a9bfc82ddc2e23892362b69ddad1b0708a7e948cc1126a9f9ad28047d31d053198b724453e4b1757cda3a0401895f1726e62b311558f2e6ac3bcf6bb52513f5335999d2ee4a801ff1d642136b579b675286298501d42ec1a672c104175fd96b6c233027bae6a35c3d87a47213ccb28acec4a24f0160dfe58606711caad26dbf6aa0b27eee2506b363ddb8dd1195d8dae166d547598f9c009731b49bf03c0b2b13babba64b4dc31ba42a96f3de0bda172379c8a5f5210801d530e788ea8b74feda40f057d042d00fc5ce03a1cd1aa2e8e30d992207f27e92abd193739846ae112c97d2c549e44726f52d81d1581ccb48fd6ab2d534f62b5143f782cd36a7cc15db637394887194226ef1a0b89b780bbff9283475da6f444f9211be24e699c9e742287c437497d38095e4dbaf69b3d2e0144ae5a02bec1cb258e96adea9fd46a82ea4c89f74e2f528c61614e62bfb4bfa5f6d692c3a936aa37ae803781e1a9be57c947c6fdb36f41b76a7753c56850ddd481daad0d86c334498f941866c326a8baf675ddd7a7d403cdc667970a4e688c15c15938697391a62d01a856c62c21a1cb1aa407e955529c3680ebe480b529d247dc92227fb27e45829586855111454e015c876fd02a28053250f5a5ad384463620cae4d3652046a2fb065cef2be2225fdad657681a6ae228ffa3bbe5b7c43ff187fedcc74e0b5b442b37d6482f6951796b096f875f67863b9093d7405cd037567e1271aa542783527d276ef903dc1a1c0d5b634e796f946384619e70b2d43343f52c6c18b7d2a6905ca59d119121041bc53eb1c5e5953854ed384ef0428a9928618d8a75e29b246ed4ce55edb70b94f7e5b14837fad01ea60aa022b49ab0240f0709381fb200f4c1225f490a25a0238ee18ab360a107581946cd28bfda729c6bfd59028ab0892573dc2314f3b90bc2a70ddf50dfddd2e0c45bef5f046f0ee8a74b6360688aac8fe54e88f98a8a42fe312ed8f944158bab8ed8c39d5d20fb91f3e04a165a9194a18d9b17243247a6d5a11aa26e869a3a364e4f1b2f73efc94d247e2505a30c0328b3c4d30c69c4abf1f00e60a0951a2380f12520235e704d1e49c3100491ddf6e19815f022f942a9296adc9352a7d43ded7d84a8fc812b0a566719212681c7d7159a99779ab2b680d8836edb007f020ac26d57461a4a7f4f3ea3430b9335281d321d87d43bb0917844ee7e53582216936211ce8345eeee0a16c4808c61f124a47935ba16db68cc063f2455ec7acb1516c930e7c254380a8d0e0d5a969c1c887b22394f01f15ea99d90cf6037ff03307723269cd27f326e4715028f2418f18b7822ad7cac6a6acf15d3b9fd466b13fe5655e405c0e75d6dc556414e7d854457f8963b66b88c41d6f8f3a0de401c176c789f8a04184ca625f68746502190ce474d6cfc7e28d45d3a090623dd1d57236c99815da0ad965afc649d1ed1a0be676c64d19ce18ee1c804723fc0c3ed2da13960dd3be6ac0433bb550b7e205d7b0c9cbfa5efe17dc922c5733b58fd4564820ee574a8b4cfa1309cb6e5ce2d8e0368e3726643454b65e87c8b9d5dd28a855a0ee0435d00e4d48a8f6addb9335daec16d0276dc29c64257276d06f15b53fdf3dc6f808b1fa88b793dd1b9ebdf9e5bb544fae4d709931975be42e41c3f60074001031124dd708ea0734749014b9ea2f24465085e79baf617499309c0621495c4d250e025cf99e22dafeed1beae692963c3ba128526696a861eeaae50b2a36b605748f1bbbfec483eefa10065f6489efb8ec01b1702e7dd6b0cf56ab18498fa6184db4ccc0b3918ca0c9528dfcedaac42f329425b7a031c1ff16f6448d51fbce55ec31f9317ddb870980a5d9b3beba1fda601176d4b51225be4099676f4025634379c56690faddb06de7ccf0347e2f36553a11159db3f7c644976c48423b69f222ef82867c2b452a68f79c21ffe6542af9f0d3c6f9bab755bd344b017c6b016b1bd183bf1b904808038e510576f7bb0ac41dab8ff46639c5abcbd9f879082e9699468e26373dbba1d39f35ceb8863a59e871c842693ad2e4c9af029662fc0cbc772aa732a7ef71a83a71a0ef3523ec678bcd58fc8e1c5d0a912d2f7e062896b14ab790b374b7cb2be45573922382629de04510115e861fc49cd4ab92da508bd9a7ae99585316261518ca023544806482171923c1200d9601e2027cbbdc1b33a37a00df5e96bdd4f99b1ac02410fd98508f801666f49328f9c966c8d247db1db865b4fa24dc8626538ba170a7ae0b26bc55ef939e79b91a84ce21b3177b00bc747f4d13bd02124ba565be51995fab52cbc4c759b42b21b6bbe11986f1d86c12b89119bf84a1de00e8f3d5a3b2419abda1b5277d83310cadc65baa8ba9160bfc5deb1a313bc35c0dad7fa83ecacbef7058ee85cedfd5773b9299afddcd95f1e304b6f76c2ef4e20d6d548fb0e08326dabc57f72dc4ce4095fe14da8c00527cbdd2201de084679738827862deda58ad19b2ee890b9630e2227058c638a8cb32291a39837084b86a35901e2521fa7ec3357aa241c33b11473e5c6ba58f266ac472cc479562f5ba612c7504b0008231c485f1126ef3e8ad2788494302daa5dabc3d01572619eb136dedf90768204753b3fa9cce627d057295c5abd4a2171463c38aef7a4b29790cdb4b780b463a53aa7b925485600250bcab48ff714b5fc406330e96ad08e3add847adabcde47be4b7750876ee920f3010ec47a15671c15c2285f73230cd8206eb136693f6ca0e861dc5a45ab12dd806e1bf1ba28edc427259d99e4b75ed05483ced427e5388c48c5d125023064ee9f4307d26e8aa776fade8207510190028df6ce2341a16632419f708ded43e86ccda0da2d266b15b46628329a5d6d5020cb70d0df40f2d372df18b3546e7180867914265ec65567a44f827e01022d68f5eb41d05e32045aa85e468ba382e6fe46b327d8eb5e539c28d4e3ca7bdf262ff2511bc47bda4e3104474b39a6fca24e3bc55c113089b7e1338ff4958cf5aa2ee137c757868f180f86533494960388220d1bd123a543454b927eadc837e87d921cf3ee9e38ce539ff6ba81a7607254fc2b7bfb1991ece48220124428f1b166b06407324d5d5d67f0075ce1914ed472333630555ca7fc048b804809c36f88e9ab4cff85da118a443f2231e114cb8dbb9e89fd82a6c97e0863df5dfd2e0a884017ed6cd9dd78805398bd27a73151425be8b055495555cda430c8a354750e382545d26a4de72e616d752dce657673390b21cd8b20775dba6bdb596f7b5dde2632c3b103be02515b30350fa5eb8e2a71b30052e2643f4a9ced0878611b17fd1df208b23a49ada8557c702f3cea8bb1469542512ae94b6ababc2ec94b46ee425041f99db8d4f401b441013818b7a97fa824a2225cafaeaaeb3b332f80c4e336cf8c02b0d1a2d94627ef29e647984de1fcdeda761d7eccacba2b6768584d45d0cebdff46671e3d63d15f01419b683b92e2e9a3c454f1f1ed4704c2ccc815b767376437f074fb5e3b83737d3130546d5aed033a7498a8a02e3bd01325e107a673af5111eb78bb95a68bd628bef7014f8c0fbf9c56d34fcbe022d0157197c4c31e969d4ee94805dcbddbbcccbaa2f8406dfd7b5c8bf238e3fa6008cb309a6d96f1ea7b32e5819fe3af50714734b97e4920730f826b8f2473d599eca15f8e9f2aad3e1b8ba0ab977a472897b3b7222adb901092ef9ed3aa59456b9eee8a8185eed806b834308f3be42dda5fdcfd02e46aeb5a7a416867ecdfda0072097457b8299f367d28199ef493650e9089005fba9abec888e0cc909fa22ae2938ec0a331af128a9c7ce9ca74b7d24da8be07189370fbd9f6b7098d77bd27509d24946ea532ad5ddf56a564f3897c39efe2979dc6f74617e7ea845aeeac9ec3fe1e51916ac8c18428dd5a67d819a553a68c446a58a35b35ff57195d9e506de5051f3f87b4916fe3d8428fa03e27688e480e610bc6273d48a7188bc945535dadf69e009326a7c72349ce7847f04d724ee639c66a1757ace574c094fcc8bb958767bb91cc44b2f2ac38e2ca2d356f4603be748b62c9252db84d0c9e757c21e472cd2dda3eefe8d9d283330c6cc99202d80047c99683646033f9cedea632466f652559a5511abdd1d0162aa4baca6345c14d6533a2d817e4382b9f2faea1e3393aa81202385aac85f3b3214678d1253c0213a9aab47871797343f65ed663e7b754d4183d81660041c4d5a84223c117b35c91236263cab23bef90c9c4964c4b3baeca5f69b504c5af586a3e9f01e600d01240f730c1121ee8a8a7d0be38e175421398aab5eb17fd3ed3c75395874c1ae2341347c8310c4f3102066c1e57dbc953f412ea2220387e43b2930b5666dd1bf8e48ed94444d22c75491ff693c6934e774d0a89fb1a503d0ce0116c09a3623db7eac8bc46f2cba28330db29057eb96b934fc79fc8da624a28c1324c58b30cfbdebb44e88d0862c9d7ead3d45214da246821997c4203f573a25647949005f0c5e5e7fd486b756af3dc0e39fbb1cb478fa6449f702db34e5da4416d8ef575e5c4c08fe3158de4afe7150eee641dc901f4e1f01191a92bf614c5609e2cc14cd4e98746884c9fcdde20f386762f9aeaa36c632134a04992797b28e8a56b7dd6566a6d2b5e7dc9307f436518a6d71586e46552ff16fa89d3716dcf7c9145f57dcec994f5e72c47301510b44991a5e2ad056cc3858d5dd79b5d3fbee4d740e788b913c2a9d26a24078ec431ec1fe537927260605d00a886f1fb2393f4b60cb35b3407300ef62557d845d4d136171b3bc18e288c6862bda7f51827bc3849afbec7d78b3c09ff34e40e85eb1017f57331b1de520fa382814c6421cb60950b38b0b25d201d42015eb93dd02bd387c535e9a7bd820317becca4b054916653bc6b614fff055f53f9112445b08793f23b745a9ea277810af740a5e2e99e58c966924a19ad9c75d2da1cd0a01ca68e2ae4964ff5ec610ce35b9e190ef091a639cf5c36173ba79dcf73c37cd06f083fb78a8118d5659db4de7bac7f38d5d5b16a30fc68a4bd6a1597171de72d7770ecd15a2bd730658a36e399adbc216754a4cbcf9e22232ff6f23bea37bb15fdadaaaba27bc2559d5e314c80286029234e2d208f2bce206b65a95c37ab8b1b437de6e6cf566fa8f7be22bad3baa8e64dc10bfce20909e397a4ccc955709b071fe1e7ca49cedbd5874b8dac57420542f534f23efb5bc1e6414e5434a6f1d8886c0c878526abd12ca6aae370ed1bcf2bf2efbe39765797c72c7268a32fbfa9001c1ccc11439e36ca53f5a9be9793a556bbe509a8e658371af1a1255d73ec162b54c8e2f8fc8f414b0f1536149d4ed334730a1b01a16a0ca7e8317c1b75d31e289b0ea0ad2d1fc28a75d70df0503b1825e94e6970c0d27fe9bc940c587e06ec64a5cc54dc0ba51f9d9025e9c22f9789d36ae0e591f3ba7b18615ffd1661dc53a1adb2fb5b28d4167c4ff749af30df7424f656a57fda862eed244e65e2276c0ab9d0251a63e580256176531cd06d84be0d92f7f1f215bd93c2b284dce91e152255926b3886d6e9863f7deb1d2320638c503c980b49687f60371af0789e0dd3c353a49b02e8e4a004f9dc385d6b54755b6e4c7ec28db53e4810012b3be56f8d624be0dcb87088f5fb611c286a6ba5acb5daecce66e4cc750792a05f0c05c7e1c76fa4d28023fbc65d58a7e508c8c173fbec071a218c83824f90fe15d9c3410bde6cd6a33d3eb84d3a9a3c163656fd5aa815f3e854a7a8521e33ec22d8692ceaa684041544db61bb36be34a1bf0a8d1b0a4c6b1efa2730401a8faa410337e12a068ca149b1a6bf9ec58b3d4f64ef3048dc130009e88217ab4c644bda4d5767f9e109842cc2a1800828397e38d97d16ccc2e49e6d512bb6c4128eb4b4a7eaa19dfc6d3ec635dc1a2e3aa47241715e8eef468c9c7f959b98f0088cca5c2be1ab1a610ef30340bbba19dfc6eaf0a20c907868b43728f4c2cab238e974a08b47e3fac118573787590c5b5314abedcf4dafa9ac846a77692d252a644ecab688a344e8d014867fbdfa2d4ddaf1e761699122a8d743ed12fc85603962f3b7f52bfbdc18a688b454f4b8a3674a77addf183bd8ccf189fc33c67b1d639f41c9a42cdb7d8db1fcb462d126c59814a834f47baf041f0b8137c5c27f2180f944c40757db4e563995d288583ef9806f8223e3d4819ba18446d029f2264eb21aaeeaa0925e8ed2ac96c8bbe545f2f87b4eae072bf005ebd4c6e4f507f9a63957301f4afcdc28b339b7a28d818866804c5fd51ec033a0b0d80a86aeb99e993f8c6882e0b7d08adbf8ceca724fb73666ffb1abeec5cccb3591563ca92878af8eaedbc21e5222f03f7350122658a1ea814ea1ffc97931a45be492bba87b614e86e1d8b15191b18f79b72935ebcbd20d92d6c6a24fb3186841a3fdc8864721378b824c4e6f1d57ae8ca4d9162898e890b03fc57d8f6422ba4055eb9e9903c5660c91dfcfe8a75da22b1dfdc1836b5ae55b87221fc890f4b42269a1e369bf205f29e8ecfb16c2ebdc1fa4b84e0b60dc1bd5541a827327589bedc90d6d7bb9085a3970b8e7f943a3f7258e432d8364d38b6072a2b242807a170449ebe3a47d01579c51edda2b0b68e813d2f81ee47407918f2cb353d2533ba209befa66a9d60cde5daadf1add80ade6e2f9c5c85135f3e14bf7317be02324093be735e2907767a7a6529ab19c4b8da35b62da54fc9aa3c5fc5605fb34dfae73c37d26a0ed602208ee78eb213ad898fcd1ea1a09c23814c4d9c28f11400ff78076bac25c7b86ce5dfeac24551c9cc16ff5283266a5d143843518e8e5cf39263ae53e289bad0e32629b4047bae0cea5d041661432e53dd0d265cbba1b5c4424f5c47c56cf86071d4409afc2950e12ab41fa2960e22d2b8acf734ccc11e3ac3e0369b5ea6a311a14afc3f0655eec073cd68f048b8a823a70344b54f3bc0b118e3d5520d2dc77e9fe7890c38bb7e41dac4619df0cf8a4c0fe618d67fd0e9e3c8f8c338a329612234cbc2350a11b2f94000eb3508c8db48cc29a5be4ece05a3429dc06bc7636d1cfc32bd1cbd0cfd12bb75adb53063c22139936845df177fe8d789a05c458b8e7f126f193904b216cb475a9f70b23d6a85d1ec93dd9017e000780e7dbca39ef9a3203f29d8b6c6ac64db8ce55c88aea4333b0d767484bacd17132d4c69b8c78659523fd70e65a152ddeb196dba2f9be8a6e1c91a110965c32f893f8c3a1f96e21f679f9e45675dbcfda41fc31bafd5abced8d87c02887abe32c0e19b04f9dcea418f2c43f53c1666cae454503e0aaf9517e844b059f9b0841a8ea2ff0278b30253f1b7ffcf67675902f414e575ba9f908f5db38e47482f5eccf8d7902506baf76622b3860fdf08eb6f40cc253a555da7783062323667bb69d40024a1b1c155695c543b0c75beb0016d46bab7b6081f12d357795f13a575616cd88dd5fb0e53e4a7343a11fa466e1e25bd81d9099b089f9e6dc1353f8300bf4d97a89e02b1aa8b8b3fa6a1f57eba02a8e223d8317d73671c4fb3db92ab3933f4129912975b7c79c37cfafacf6e5796a5fe12cad45758183a11246f1373e38823fd1e2b3d8198049976ae141c68eb68ceb4f89fcb6fbbad672b8dc500a75eb5dbcef08dc085bcdb6d6a4fe06343cf91d918655ca79043b30af7a74b89650015089f189fbcfca389f7645872450d91d36878ce453735830420f19b093d4937e3dbd6e309b3664c08fa72c7f1182a8ae34056a40b37ac40cc02986f31a9e46168b0c86e0e2fef35fde4ab271f6fb24c1799827d2a6b7aceed3323a39fa29099408d57bd5abcf7bd6ed689145d548f5407dfc0f81558a715679807be4d09ddc170d1de4442e2d3b8f0be697733bec334c949f1a20725137d701630609020822f768607cad686a36607a80bef458769f7a41a997353e404178bc125f21bc4289cabc5a06bd447f0a8853a785be03e0ae6c695ca33fc1524916f059901368e2877ad8bce605378bfa1d17878493e27019c14094da280904525f10d404eec9600a8721879939983cb1834ddf5292f8a5218c296cfdb0cd67c8d16f78664841a304c54031af0f9b4b9342d22f314b20f2671317a0d3f31b31fa1472ba2beebda39fbd2efaacb1674ec5e2076f08b96c30d3c7f6597fd64b17c192d3529e84f90a50184bf0f31a216e8467c585d4e0f99cbb063be053de6efde5b80eb899432611d665bb60ef8a206eb8a10427d4367da941f7f219c50eb3ee0ca13aa1caa21a25bd2fd3a280562de9e4e2919529d0555c1e7268b7d40b4f0387c843cdbef1c0570a66839cc7a7b29c2ffc8026f4803012785818c594bc8f52404e788c3308c209ce1f0238b9d6cc0d91dc701a098cb28b22f7aa4a054759dd0af1dd96a00f98994d5d028604f39ae29bc3f6512ce016839df165dc3acbb59adf5792167d74b158e0f3029b3518f04b02e2a14015ce8b1b02937b7bf5572c66f0f4bc8b4fe127c1b03086363e37a2a9c3e3ab2f4d2a627ed5ac9be7e9c71b8c75fb88062746c06a3077f84217f196cc7463d3658aaf5e8a9ce5b1ea1424de4c735e514f6c4f34b2f38f951aee7923f10a6bc220080f0f43ee3499b6ece8b1586d80d483b8626172aedd2d3ce76e90f320202bbf8cd189ae489a228a0b8ff2e4344199dd99425b89a23242a79f0011d59e2f2ab1bce7af2df2b0b20a35f6f20dc9becb135957705c22d4bb26006d3b81dec887242eac19b0070381bb1b2c7ab9c85699f1a4922817dc439302845f810181f47c35ccd143ece1df18962c9b52cb2cadc567f123cd40c1a294005a92da7b5f03c12a09dc2de5c414587ecca48ce8ae0612226919be7a8db50c04aa3f7f16b8fd8e7cacbb91e2567af0de405e9114022ccb04da806a6dfa1b8176433ed7f981beca1dd74dd48e471db3578d0d6ed14c873b6779bc2e081d72e5f91ece56d7fa94a114ad60efc643c4d8f409a5e920023f65443c83e9792f557d656882f05c1cf55916a8c1f8a6698cdcf4c6d913839f47f105334dd0f35b7ec273847dc20a890f6615754d86963b1b359fd48d1c42b64ca4a4239cb86c3a7d4c825443d0e4d60219c412404ffd3a754dd9fa64de4feebcc28e82a99530036ea3cd7bff0ce36cbc8ab54a9dd775812327a99a464580ed46a22b48719c19a581a29454d32326e98f322bd12503d5e8bf9b0eab7e203d060cfd549986843d5332033227503f36e8c5bd001e604665f5c565aa01b23b44191e9d6015f450c38ac5e4157c87e663d2058bb32490a447e87e3db4c9567bd75d6f9eb8fcc3ffece2406f09aaaa4255aa320e6b6fbdf486ff6ec331bddc10f110a75c13ce120d719b0040ee1eaa6aa02e06e1f4794ff30e1308bc1fb5d67059789a109d1e78efdf643e22833cb685ed4a3f8d52cc73549f1697bab3d0e9484e40bf895a48b2dd2b5a14574a2d1b1cd6a93b2735b4be44b2d88475ee152a76f03f27a6c6725b42a5e2d1c486c6634783a38cfcb339e378cbaf6649b6014583b4a0d4d867c52e6b1dd4700d5474c377d2f87e8013ccff31be64bbd6b1f34d145a0ce68738789b724fbb82ef5a47954f02e407fd36ba7d9ffd7464a6c733b535ffc2482b2ba06f177f1a1ca3f8920b7f38648e2dfb0079764a042082b90e82b3ec1e782808c4177a3efaed531f74b70e3fc271c420bd8e23d936575b07df1d912b68ec39c4cddfcba3c15e931871b86799a6c6b64a2b7e52e839ec910e2a2a740c3eb4954d90a3aa338a11eeea6f7e37350c073e799c2f0115454391b9613749d347c06c3636ebe00d3c916a23be3e9426a01fb638774f5b44cb200896ed6de7e6942a944338e0a505d7071cb2477bd67ebc585598666c5710185a1bb91568759e291ebf3695892e99b801b9b45f9bbdc5f6cc775c7dd342ef9ca7c8d94268e4514525bdcbccb381e883da96b6edf84641aa4c0e17c1ff301052c568c47ed12b74edabcc14716fe1413fe6441aee33c2eb581b5b9c2beafe3cb18b19617a993c53f211347969bb9855b4e76f1c62fe19601dffa4764a853acabfeb17a85e84267a354dce1b0f4525281a447fd153a6702f8d469a52a14316b38e6fe1760f3b584f73a97e22d71c9fc3ced36cba5c9990ec3fd6dea4875e67c912dbd7a778bbea162e334fd5ab649e67b8c00eb8ccf3ccc5f277b77efff7fe0b5bae9625774a888c1319274e5e14dc6415dacaf5bed43d6e68e3e3e756fd36f9d68bae79193a8bd1feeb11cbcd1479b1a944d6e2d57c93589e253ea047bd0be5acd383aa554fcb7d240851e1d84152715ef3a6073fba335eda6abeeb0df988f5e52797a337114f29e6c3cfd89934d6bb30f6379436dad6853851f77b9a38c6a7fb8445617f0f170e7e43be401fbdf68ba219ec33e1824f1ba3916326c1d487cbfb8afc17f5fc8720548c74bc361d3cd75e2cb1770694ad5b6f361c9ffa058d69c9bf85e8f3d087472a09e08f43f5d6eea94945c9d8de3060dd4ddc32fd5b164cd3bc5fae1e255b23dafbcb2ade26cad5b17be3e7fce221878a1b91ea599834b0b387b002cb973b157fbe09afa45c43545858713ffc928cd4401f4302a23f74d6d40cab6389865012e5e9d9158a3372eade48799e6d9a69f5b383566bad44a38c4a42152a5328ac6896aa4dd78a646eca2cbc870a822eb640aa878aced3994bbaaa88fe6ad97a42f413ec8aa7dbb88e09245ede93b3116692cb10944eeaea62cb3be6fd2dac4b2d0562cf6fadd5d0aebb65444eb70593c3132c0058d8e2adb9fae7aefda2e7ce8b4138bc4defe819c526afcca99b73c9afec9f89e4ea77c2798c5dcf02a4d04cbafa1345dbafe61f0ce1588f53a865025f1c76b0844b9fdea8525d6c092ca4db68b70ba870577ef08977208dd1bb1d5808ec4fdd232ba9576bb10877de05bb2a4df91e381a61fa6b0a7a7097c76c7e2eed13e85a17b11274fb3a89606888e35da7b4cca6fc263ba14a9c570c1dd5df569c2c4d289268573f0173c78c23a3653aaf247f636777bd07ed2206d6a2705601f647a736c44fd0a373f9e9663f762d8e83848869f6af49a069ea20f36178b86e220d5aabca1d0ad6e5eb5043fc96ec9a1b034107efd8c4c2d4f81036e547c4ee1fe32161c236bc915bc7021340c5aa5c4d275f55d64c1b9c6bd39faf25b82f0707971c59cdee236ddb321e0421f7d53f5d6875d9661fd6c4836269868f45106e98a149f0b1064e1e494d4814ae473bc56caf0c6d8d900ab3608831f9b55f7e3f6c41b0d21d52bf6539095affd5e0842f4b42cc46bfbbd6f0abcd62a8641f429f11cfc1e7e0bbcc10e546ef1911d9a7103bf730facf56f425ec31528a5a333df7f6f4dba1e4973d07a7b5b8b55aab064e9be0776c13694410d8ce94d12e10d400911ad65fec7e19c5f93bd678f41beff7b3a6e1cfdccc949be4d749ce335cf3d0d605064c16169a565958b95de869f12129e266cf2515370984d033666fd7cb4a5d4b97db330fdb34abb309b29dd583e7b1dbd06d629b11f02e20b3d947eb8b87fb7b668be2f76497196a16146660f8b42624b949e11f2ef5a4d2f7b780da7309a964fe2af3bedd1a6c14cabe34c21fa3cfe265a85b96a22c629a54ed58cf8284f3b4c7331e666132b3653e5787b8c73b2fda05aae9363d143286791ea4df28d2733f44d87e82f02abc59fe92488564d896b7d548a0aca934794ad406ff1c9be0295cd1fe0228a7f38e5a45310db3ca0833ce90874a8d627dfb9a4b2fa781765012b9d2beb676eadc49ea6d1ceb38c98e1cae3f046c96cfe616e9183df549b67bc50d5786b01bddf548d18a25fdcaf6a2a89fbd4852e5b378901bcdfdb9d526a2f718e75899a21ff387c3e4c4ad2bf709e6632ae94ca3199ac96885c5e79fd8d4e00b6fdfc680417b16ca327249c1a672e6bdb18c25471ab21f310e9a2493a2966ab2eae0789b109237c87ee4fd8a0c1e66bfcad6a50a47a68ebbef22a3dfdf14520522d0a7136be284129d57cf06eb5ef28eec5c37db85427bb51a7c9597897b79b242b313a078f775dd6add1dffb10cd5134b52b7314b5e62879d87fcfb3899c68ceab96290ddd1d2b0918886b046304721fe797667f8a8b9f7beb161c01ba24ebf1e2e1e6f807e059c6f185c7d83e68a73bbdabfe2484a8f69fccae2c44832b0e34811875bd0f6b784bf6485291c6013e8f26c6452e43b7faba3123e0b792cf482d8262620ce207c45aa9ec883aadd415cdd91597a5b0d1bdb2047af0d73890ca1c25b03f571fbcd221fbd01638d8d589dbd41822911e8e370d9e0416ba6b86e3e0612d7eca718c71ff2cb56611aa3e1b6fb616f082c03c4342e8695fe3a340d956d5978ddcd1168f7a48f3d9f7b1a35ecaf9296f7c31de3c4200be456b92f7cd70f5cf2467ed52398b4787e31b19844d2b0669eb5fc30d10550ba817fc76795f21fc34f7a3d20745e0cdd5c8d296ab45eddb7e74593d2fc57f69937083cb14f665a1ef87d09a58a2447cdf86c2f3e4ff60e26e22a9304f59b9a92dde5f8a8f0cbf52c784738305e21d293b81a6b14e31702c7f4e933e376d95a7b639796c614cb0e1554ce02af40191dd1288336117472e6e1d2623bf7697f49ba822adff62b33212abf97f95cc9a229d16f62165a3d2b8b5ecd208723ed86b7d06987215be082397c48f33df859e9d8b9e015d188142daf720ce06d199763bdc606eaa138e3d8ada23b3bb39903710308927e05d6154699bcbe6d0e1d8a52eaefac9e51f999e610a8b26356c939371e40daca9d6cf25522625506e8f353f9d6824588a0f27bc8489df848670893b89430f7c77fb62c04249c1596ca84d32af1b8288d16fecaebd74042f77a06e17994826112233a6c7ea9e22476beb8874f2d95906b3e6c65ad248d12b07dfb34721d0d5b2e705512f7a85399dec7a69970c60aac5445356f199642671423eea4df6f3de696ad296ccb43edfb97e4e3fa3d54571ad108ad6559c93687092e67aa0c7cbdb0ccaf2e2d18689a2498145692132e5ca75f29478cfb13462924e7609e16e1197e233d7d10a7dac39dfd2a96762370c84d2d9df0230ebe2d3b6f632422581add9a30540708caa84ffc342390c7384cff5825bbfc995fc5df1583a0b2eb1a6621d7c179c84ecba764ee991126a636a365ecd78404aed744b3ad0b8abf61bd3257cce83f5aa453fceb2f9c1286ab90a056900db04b3049bb5dec05d11f7c73ba0efbc0a8ff12d8eb5148eeac01fcfcae2ddf0e69bd055694a85f62d17e5a39a371464edf7f02affe65dd96bb58745dd89e6ed8de2b037a07f96531cf43e27818eb2f4be12556317887d24933054b0b0dacaca06290dbf735128a7ed719aa366af46d4146095ea3cc948fedd16a7cf240cb13ee4b5365b31e7479a23d03ff1cfe87a0af295ba016a640a4bc5c5152aeb95b4e69395c5ca4ef077164f9ab2e943fa3d28f3d1bff1b4be80f0fbbe0fb49d4767b8f5d6ca47d80fb23e12b430743dbe69adeea33232b3b7799bffca53e6747d156f4ab8cadfa85a874ca9f96648c9d72654cdc3bbb7cac9d6eee762bda990b401f5ec9dee78e854c0b91cb41316132207122dd2b5f3625cbc79cc2121baefe1dfb3aea46b01bbf61784f1abdee3d72a676e2ac65ce990b1e4efe657d9c97900662799241d6cc7bce3d8c1086b79c69f6610136bb258e60e8fa365996432cee8896a3abba077f96c52f971fbd92d490eb1e7250d14d858016142fb5ad9e7296da81023b0793310ad67c7f9e3f87021aae12a6953138692f0e9342c85dcf231b30505622b75dcfc31abe8ae33dde60364794cd4cbd58c21b36417c82fd4ed438ad2af8e85802858f07cac30c625d8a9eae59a1a514851d767171d81bd5d2d9df94129a9451cfcf4b0cd889d113dbe3d6c526d17e488729b6a5ba1630326be8dad9bbae9b94e17047fa2ca7d37c0ae675cf675b80f09191033966fbe5834ffbec51eb2fd9f0266da07186c37a96cd44ac2f0f2261e8c231774bdbce0ed7677ea47662f9ed627ef66de389bd0f2d0281f8e951dbc5b217b0fa7b959938456abfb5c980af254cb489eedd6c08817b6174ceb2322e94f01d38bbe16aa0af423cb0d29a612d6e2eed68122ef4c013c21110985e1cc8dde22fc4d879d8c1476b780139a4cc59011c32ab96fbee0b1c6d5251b18a42f8c9ae5c735332a2c677dbd74c04d93160d462d0b4105098a07f64060f0e7f66fb27c0deca49d01555e46dc4898ef68be9806c796b9d6a597695fb59c8243af4fea89a1d222ca275a1d0db78acd518bf1fb23b197e662b7a57d0699ecfe0f86e5d6495bfb0d2569c411c1cc3beb415cf66c1d3cd0ae924164b768e24b8e98b7b98a718db622914ee8db2287a28bd6e322331f20ed265492c9ff2c544ebca235e68dc556e577a0fb4988ecfc8ee724fb86839c4ca2e69495632d1490c34ecfe4a5707f5f3a9c628f820a6da27862d517c5e6377e356f605f8f51ad387983b985df9806573cd89cd253bf4ee66e554bf33a2dc4bc604425b2ab4fad3961ad160f9bcf4e7f40773881239aaf61db13b57b8eb7955974f814994d7f3a528272f33b4bf47f2dd87fc9825f47b342cf09bb41bf1a126cac4c0023f49177de85b3dd53f1095dbd472b3219e29ec924b03c5a470c892483db32c4b62e3f73e3fd1469fc3de4fbd9d21a76433e1464583559da4d4b5ea577b53c5e216eb63376717ee677ebb290f76834de0f90809f3556e8511dd09371f0c76ce8f30c53cfe89b46fa36ed7dcb5ec8f0f641a9bdc385d91e2627ed5b5506150f283f228b1bfb0c70d6b66848536179e14d4d6bb777c57165235257fd1194405e577e6d512412111faa098776f7f3ca0734a9f6271ce6a9d41dd65b4d1db7286392b852723f1b4d790f270981ad4e121f14171565bc6d9e93f5326cc841d732add7268eda19fc757563e1465af5c90020917652febeb3cd2856313093f859d62eef3d946bd47106ebc20765cd067abbd349748b4a1b4bd32cce27613a2272c8bb9f2806bfd6a24e895b1244f098854b7275c4b0a0f8adabcd8b693a8bd87e43e25aaa2963d2486b9181dac98e32a0cf333fa59f105e2d81bd054bd574cdd7024a4acfb2d3a9a862ea5ace21e7f1af45054fc850f56b1b1d83ab3a91b9ebc3b0cee105fb3f0d25a84dc0fd3d95a50fc84a029394579bc4e30bd049678295c41321be892112a2ec1356fd45c74f3d56987a9ecf2c99bb06d03e531964ec15b6b36f2c36437f16febed37f04227e5e43fd01d905d731bd42969f23214799029f4f8d3885e158c8b024737d685a805f77dd3ef13c4c64f256749ef3813c4d2388479f2c97a023a35908c9353af62db5b4ae4feda3352e7c55083ab9d89b624a44ce439ffa7c7f1c9eb68f026afc8400d731b7701f1b4e362d567c887940a75423459fc599fd8f7c862f938f3263e9f9af5fbe0572021f56cf2c31065a22450588a8d74240fcd43c38c280d09d25ddfb6b8a5f2aa1bf2a844213aeffa853f3685cffd4a0df3081daa14592f71bf67e96bad78ced9c6f7136d5f733d7e3cdf5060ce22a928b85f996fca474f443577fb7539dd3539ddd5e8755fe26db21d1b462cfae8b9384de3a891e8e4826721e9e35a2162e25403eb86a0e8d2dd05c833604031b1443d7ea674fa0a8b111341481237a81ff7a3f7a26c243e82a74dae98f06dc201eb7a7ff5dbfe57bfee44d66ac869e02614998b41636eea6daee10958343b41cce1098e9d5ec0c42b85b83e118545cb38e072f7cf7de7bffb01127c079d64c6d5310ec3e804a734af98809342136c40b75f0eb3086cc48218a9c3d00be4eb9d709c1b679a573ef3960c48438492e6b7dfea148eb54da5d6a79a13c068eda3afaec1fd16a7d8b8a4619a78bc28718bcb4ac68b4a9268b809cb0984ba47aefbd9042c766b3642e8a3d3d59a66209b2e4a86fa9e5f067759736ea2f713fff86150a1d7526fbf6af1c24d1ca819d36f7da01f29d9962dfbb12e4151dccbe0ea27603d23cd9a9f15d64fc7169f72b8863e119959421e26b4675fb8771aa6f0307cba216b1ffeba87a4bcfdd876672873692a686f93e2fb8a16bf051dea05ae3224eb0ec1d95196d6fd245bfcbea3727af3c43c8aa87c55b4213480ea15858d075516fd6f6a3e54bf3cee0829097da9e832246fc00247ed5949d3a6636f4897695c9294a77cd7db86edf8c1ef061eea46477549e3f0eb7a4d5c425652fa7eb3ccb5a99f5d9524ae21bfb7195fa43936f2795c79b1e25b6bfc799643317076f3451845eb29914d0ca8b25235f1b7cc55741ee401e2bf5f20524e090b946d7987a38a9f2b74d7ab372a3398919bc850b023d8f99943f67b7ff229791e4c954bd852384b1e38e6c1e7831a7bcbd575f01c8e686e6bddaf6b2d44581fcc9d0aaf66faf8d7dc416d9cd1c2673e42a8f3f3fb62f0a7026dc199c340d899c7895549fe4203592b2e89e82be0df7eaffbf3f6e1b2438abd157cc8d0fd00e1eb87ba5a88737c1922777cea01119da715b640f14e469f2129a37bd36af3954f0c62cd0c4b7d3eeedb8c923249d15aa8f266dad45051d227b0f5a9042689aafa27ec15b8df1fbee458568290d0f113b4ef43fd7a783cef08fcc3d15e4ed38a05cdd8adefa91725892ea5c645fc4cf9eba91a82ec8fa2a7362d54f46f63bded75c4e1a7a601a328f123dd67c301828333b7b0a57fd69be0d6855934edc7acc2a70db43399d2fa1df5e2513005ba65831777cb7f725d4c5d6803fa1e51bf1b0379721f2dbe9be76dd37c410442addb467ac7b6403ce6a47c0a6fb8d358fa2653de18a7593e0ece93843b35541905f755da1111c7f5c4a474174286410897b03119dc3a905fab3c681990d56ec98359c3ea6ee48dc4c8399d51afc669c5710eca56d935e3311b31ec7ddb4f74159a3ba83691d86e37c6e51f3dd31ccfc24589a9abfee25d4cff3c02aa497d5ffd824d4b48ad5acc5fefaefee80b985bb6e0d058a483436cf4cfac34b46b378f3e2e2ea75f4aff2025a71f1f9791243544db89092fcf73f49870f741d4437b09a03141ba6390a19020fd7dd5b3f9b3ee47a850fdb2f29f058f442714a041cd3e8f7fdac1b20c630325b0e16287a823d8e65799722cca57f2106c72212acf97b61c524ac65d8872ae258755d754eb57e6b539351abd3583f971e4d84f306d634d926c97b1fa245560393bc6df88a1aa3bd4dea59ca4493c39bf5d5afbe053b1fe9d4be96e8ee889715b7a1c2dbc9be7cffef3467e71dd2da4aed16d6726849ae3c8773acdf419207378d0afedf6e427a807db279fd29677d27b870becdaf66e5051d143783acf1bc5ce51e0f8809e810d6a30e64bbafdab9f1e00f60edc1a48ff6cb007780044006af00100700048e35fa8d2654d6418c1ae0149400e48e4bd2610f7588907bede087f772ddff41391fbd65fd1324bce5fa4753990aa56aa39ac08647b78f64e1c267c915b5e90b4d3e8f42659864a4dbcd0132eac5a5136b49b2b4c51dfd562885a25dc897e6b64cbb82595089cdfcc34f7af94346272d094007ec4e8cb03c2ce0ce33dcb8641245a173b1607e8e3b03884e7d9c7a7946252f820f70aab2bebb033e521cbae5f98a4452f5d2568170334a8893a71bef2db8913229a06af39602eb8994abe454fef464d3d0df88ea23798353d41931f1b68f1498923fbc20ff689a689ee922ee1ee5b31f4777244a7fce551a5d12d33becdabfb3862213609630b0661aa7b254e1c5d542e883cb565c9f10a356dcbd9a45e65f79e791f8508f213cb5d61592c38abb6cba266531da27f9bd8f1245b671ef427be83832ffb988ab816a2777f54ec569903d63ec6f0482c16a8b045584885019bb8d4e2fb1a49644c1fe6e8afb0aaec60add02ad050a75d25ef6d3980423bb2bd93d563e74fb1c59815dd5c8bcac816062f8f848460bd85620d668b7559377066346873a3d9b2691fc514285f42e2055fad49303613331f749b783cba6b3504a5b51c88f0dd435c741f58b9b484fada068ebb277ce39bfd75b7327094ed9a42e0633fb1bf0b096440f1568453735ec8c0dc949cabcaaee38eedcc73a34fc809082778b39e1f52e13d442276abcddbcba29163c350adfa6a2ab5aaa71a61d43e628898df51d7549210c5be6f47cff37ddffe0a82a1a9cb5e147bdf4db7aaeb992a28e520a6f4e60e1f1259ee82a5c2e8820e3eaec66e9685e626563d11fb79995f4690cdbae63a5b9cf3ee0945421ad2da4cdeba93cd7ef63a5bde93a773383bf9f1ee68129f25904dca243bcd9093b7a1273379c948dc23cfda744d86de4925aff47509bbd09e2e19439a6627e06c9266197bdfac7841ce4e6217b5565d72803f6f23018878b0feb36b809d23869de79c766c37edf0890f5fd2fb2167c910ea5b3a13171ff4dd1e15e13630f374f9ca18b6fa7c83e0e6c5ec13aa0148676d5fbcc1b08b57471b8da78ee4c6d6b3589a0fbae9361bbf83cd16c4ef1d4f864c71b13d1eb165b4be6db4bebd97be5d96be25d2be1dd2c92ef8819e78c5277f6d7eec44e6f50569e57d6b93b4e632bfb4e0f5d46d57ba8496629c3042b5aa1e0ff4c5933f463946c6a0901ea5206bb867aca90afe822e9984e81aaa0fb4967c32758f2a3a276f6a5b698cca57182d099b1bcf754d078630f7df6d1c962fbb377274c5b9d38f853c36a4527ef3e5db6f758b57b48af95fe7e30eb79d1be6124e509880cca28acdb17555570e3401a67e7674de3f86afa1988c610e902f75dcfcfc26ab457a311c3f9beb8450fbcb5363baffac713c2303372438b71bcc1f315cc531934871ecc70622fa53efc0bc3efa2634e4d84b1c8c0271539b0630e136df41c21189307099699e82297b9fda91b59080bc23920227b1bef7a624e93713b08a0d01d5fc9ad51c1663cb2d375426f29ac57205b4b4a520ff7df79ac2060454f9a76901dcdf49bd4b020614610d006f3f56231177affeacaaba65b2c24ceecfbb3e62351cad01f8efffa8e66dcc8efff6eff75c55fffedfffc7bffd9ffff66fcd310efff83ffeed1fffa8e6e9f88f2a1bdbe1f79ffff8776fcee763fef73f2bfef73b6b87fd3f86b99eff5a9867455f6ff3397dffa31db3bafccf7f9cdbf0dfbed991fde75f36bc4cf5ffc8b3bda488ffad8d38dbbb115daeff6af36af9612386f55fff3fffeaf96afe51a8deecf9fc99ec2867466208fe278303aef85fe1ff95efff19d72a0df3f7f5ff664b1c701bc52304509b0a57277fb05ae1905af1088d0775a570f71fff9feb0f6ef0f5fc27d6136ee60ff6677d253ff7df9c7f38fec4fcc1fff8ff607ffcffe4f588526ef6bff99a7f69d078d0577fe5e490bff3fc3dffc3f53747293ffbdff6dfebfef665d253fc9df30ffec7b6f8fbfe338f252efc5b5f293f772836a62f727fedf34f4c2c7961f397b6bf72fcb5df546a8a3f9a7299ab7399ebff601f890bff8ef9b38f3f7c7fec3ff90bd9eb13c94bfed6fe5f3d9bff99efffeb08c546fd73ef94df9f1b5ff65fa5ff0bc03d3f184c00ee4497b9fd92846237dbb0120141a975ed6b6fc808fc44434eacd0657737a1113a483e9d74e32d90e12e52fcc606d4ad501cfb294efc20be1e18ddc5f68f3af76833cea86a8e6cb04bed9bce161a6ba9d88e0791cd5e6dc87071f8df2d6d39eacba08e7590b0bde89f6a72146685b5400459f973d90ae732daf65925e7d57bc1cc349bd2c172aa63e1d0372198f14bfddead07408d453ff4b848e932da94ff59462a5fc3591fafaefafff737fcbf8dc4c96bf75ff39f03a162e445ff2b471320c4df15c94cecc149ff4be9a3a484cf72b1e5bbe6ed1dd67c24ece7104d54d19241ae77a298db2948cefd02ab2047405407199b2d47b50eec1b332213b313c66a91af48a61ca574d584c4351c3833915479b8dfbe5c5c49cdfdf032a6ec960f8ee7916f8e91d717676bd7ad0f193a1aabb06576270e5e1707045bfa0a70b9e05530eb6e4edd7099d0ee71cc1dc4cf5d5f75379bda02bf44a42a2e146f4333d6e20f117f3347aa56ad5da7f87bd840f53fcf898e751d7b089be90d67d49c28cc309dfee4cbe652a56d161dca6837b8e1b11b18a4145ada117079843ffc1bb74d55c9eec1679c94e15cae6263e8c3640bcb37d8be4ae9ae4f648797646bcd84d41f75fa3531557e575e946635c0c45c52454147c25b6e82c7bb6d4095c95ac28a29cd4f7323d076e4779cff18f7d3fcd618d6b97b29dba56a0fb97e5ed60fc1937c2bad1473bf28ec77f8110fd1b9c9214e2af1f99cab07e9afd480a13ed160675b3f4a47999f50b3f30690d940753ff3159bdcb3092631fb045cfd62604039ac56e14d006d612dcd55ef30e03e50482f84cea4a2b482e16b8a1987c78fb36850bb3ca0e363ba9608e1b91e139c01e4dbd6cc8a394fe83ee17a209329a609bc99f3e673a9e85cbfc82abc6f10904cbe5f20a7852c3e31047d1cee062d4082173939698550615c2626b8faac176a591cd90beaa21a16680ca92abec4256929a73153ea0270b5058aaf6cee9dda62a433b38e36bb1ac1b9d4e94ab753edcc9b16ea801ecc974d0292e0e7c704dccdf12885e42814563f693d8a0b43fc66063dbc4ec2d623319f26cecf32f80dcfc189942a610a401749c8a18a26929f69706123075a662a17b4b1df96886b73958809ce5daebb99cc356a207273356d0eeb0c97e44ccc522d073ff54076f22a3b7fbf429fba7a4119ffc1b2902f8050f322b9fa387ddbdf2a3d1826ff2e4149c2b71135fc3eeac1478d31cfcff88c3f8102025157b66f4454bf17fa16d8b4a284bf52382b212b8625de9a7ff2cb7246b22f13d1006a3036eff4d437457ba4c3e4f9ebf997853903e76b9483552891dc832f5bf95147c56107f65c493750e3ec6a85b31d5e8c34241cfe46f99284f019df70f099dbc74cf392f0b73aa26acfcd7f0c83092ccfb09a7e6bdd443ea4057fd6f6a8b0ea5068b27a2458b01c12592afdfa453f115a700dc61cdc81bb82006aec1fcee47ed850caad983ecc1723e3ecabc3e7f5e2519736feac38fe356c1976e2bcdc83c026aaf88e00e52c6ee3f2a65ce6032c7e4fe477352c2f59ddef854ff406cbf0a16ea2b83e5ce91a185dd264b1e508444097e5a75a632ea4d3bc05e2608cf30dc65f65ff861e222a52816b48158d585b0ad1b43a7626636a08622aeb4ced856ee4ac30dcbf707c7e7974f3ef05ff8d04541ac74a7dabe3ba6fae829f7cbeb701142c3f33922bb7a3a388c981cb2ba9c32742f746e6766166e3c7051d354765f1c9db088d77845dc5a8fb0810675d272d7f9cfc341357aeb1b64f86e1787c78a4a5f53a0ef9c73fdb8957df988f3a6b0c117c9adeacb8aaabd964543e8b5ee964519869f2869b58810e6c839836ff662d6211f295fc1cada22d36e24d35e22c12d6d6927b4df2d9144124380f50221c818e3b0954b0613217e99cf64022dc01b97cf08b7ca43caffd8616be15379d8aea3437c74770d9813bf951b8019957919177094e09cde0e922616dcf138ecc972291e07906b37f2e8d2900060d07f8fd9488026332101291bcfc8b3a49802541296d0be1712013b7490c843a230566a9360d1875ce45d4bc192f4b532b72a73f3f364983701f40493aa972b52ae99fcdc107df831ad82943219ef69a85607dd91511b624be65d4a6b6842be4eec477e91eb60aa972a048f1408f2b064cfa7e82ef34a4160468f87c150a494d11dc9a600f766e4d1ce417662a14b731605fa7652e8f6d799caee1e13d42df6e9e4589f9baa071a1d0956d457d73185975fa2691a21239dc7720b836fd5f99d317440292340789ad8a8eddd1b726e611115037c701370151a3d009b98915dadbe10b6bb40cc12d30e715f6890012f2801758d1598c507b78f557aa297fd169ec1b62979578cfb83768b628493a35a0f2aed2628886c78715b55c8265a6e4fde007fb1fd2fd0a1670c24afd1ea9a979842006f442742a50a3c913908cb77383fa22dba7425280b40a5b129b9270046d30ada97636e800dfcd842b735458f346b26df3c7a99dc77bb183cb2c794de4aedb96cb89210c16eb4063b2d58d9c6219d50dde19b381b567cbe0593395b528f4eb670004d491b7ed71c1075e022a7bf2e326ad925037881fc6cd956b51957111c2f8b54848789562662dd7acad9e39a1e1eea50ff8d6aa0b358e544271c83d899f7ed2074931e0b5c2bb59ca45f4b691d9243562dccc5059e14013ec796a28e4c4583e8fa92b4e4c7cf91d943f59440014a937b4f979867ec9b61ecee5a61b77d13621b971d7d110e9b640ccb11353201d7bc11937ef366e09c232bf0c2d30bd88801bc059a9771cb151e6f294efb0c29be499690bf0ec8d1911952dfe6b49f5234dbc07f79503196bc71c584b516e193013f4e1081eaa20449485216595f3cc48f4cdbec40c4b596263c1bd8711826735253b27155c6b7c81133c1ba90d30f44b6f1202666d006e7fe0536ecfcd9648f697a6771bf31d6e4da7fdfe9066728eb239b5170edce2e27cdbee76adce5ae69b024185fdeb11e9020518e78910a7886bf6c605de9d65359f879027f61de034eb3a0cdc9c8ee5c3380fa03887bf99615b9568d7b67f1d2dfbf2f9dd89185fb6718827ca9a771e6d03c5fd96e7a0c1a963c008f6c0f0999bfc3603d6366c1922c98a1e9823c6868a88f7db25f89d8f764ddeb997e18402cdb2fb349f2280f8a2ec8caa8eb5f46d812694119b3fe290b674c3dc93b29b119c10df71fdc22ebcecab307c3d914f50d9bf98ee41379f86d32a89bd42a9cdb2cb9d69333e091814fcf608adf8b2b901f42ddb2930a0291b7ef7aca1024682a99f5892a1091cede152f3591c98141230c2828c7d85bca393edfd5a193f0cbac47d9cc6fe79707dc010445412c47f805d83648fbd5111620744bc74bdfa5e7e0a0adc19648fc462e8a630da02619c13fc3638d4e167ce8870b7f01fc02ae3d033e8d56088031fa95c6359822518d07aefd130810cec1b2e45788f9f1a74847177f8b11dbf5d70cd81092f58f6d269855af887a2c7271d33f8c06b9b2899cf21dd23dbb0945ba9139cc374445770b549f8d6a356868a495eaf6c28cec7201a19ef212780a2ac39767033112749523870a22b722fa8a9d6b4deb782c131a10303d9efa145a48abd59a48d6cc0bedafbdea1f8ede9f72a8f2ac3bebd7afd5805983db0799462e3f3827f702d5cbfb7a3c8d4758a4870ee9cdfc4e8d918d585f1c947c5b6422698bca5840ebe03590f2810447e36898dd73688afdeb0a43f0b5a85580b78d2c3de0d892071ae6892471578307d2da76924f20a21fc8a9ccf99f5bac61a343146be68390cd93e17bc0df0a81f9e7c61a1c8a90b09d898ce64b896c6fe917670f8ce977150f85e8ae50243b432d22c585693f612cc218943148ae7107e1b1fb39cd0ba3003b91682738e199dee1cd8b23f32170b15f8f0915d70559d2b104b751081cb2d4c599fb56244785830c0877ca3df1e7090c0ba729ada12d01d57033e80364cb198b5884c9d7b7e3c4b848700e6584edf144f1556de04a1f1618a98ae7e2953414e023a20396ece532fffd6523e71372393c657842353f8a8956cbe365c080575e0015d7e06349b9e93b6c99030c5833d0ce49b70a1f8e03517772adb422817d4bf5a272255b8471ae97f1cd60cce1973ad0e69f8d6f63003bba6d36ac7f7d3fd88cb4fae94d3a24bb1203b3021c0e7a433031125f706e216e4dd97737d804addfc4fbd7e142b9bf777f6be8496985eb85106ac7d6a15e0dfc0634a413e156585f9ba34d8b81636cda1948bb3cdda943d7e773c477658008f18818c53054c6d520df5ca13b6150ede87b51ea814a5c2edc81e1040614b28ee2252adf1dc618912c75d20ad5b5989763e6c840bc6996395ee9912011ae286bff39b8753aa89aa710e129776dbcbdd6b4ee15bdd8065980f61c7f0a18cd321360a2716d4102390e6110828ee05dc2c125f582e5388f9b2409a812de3aa60c027c53abb2b8c8047250c2fee7a635d5681ed1a2b4274a49cfdd74053a506bee45998dbe83b040a35996dc4060a8499a53c5bb76ea35471d6204fcd7a6f1e63a9d604d06e89737db2aff47c91ae923f822cf44bc6640ec6e6c685e8e0579f8357988fbf4035b041a25ac9560116dccf412e4787a71de0ae5ad04bf5721f0eabd178569b1d97d3b9e64ad3f0d8498cd1086b2a1dbe20fe00aa78562fc171d062984b4208891ef8ee9895987b83cc758a313448e49b86df9a1bc08a03abe96ff3692359db39941f4dbc4563a963e78b7a890e1906ae6905d215dc4c812a4e4ecfaff041f8dd665743728e07687b73734b0ebb68e8d4386b5dda0ba4acad0ac9e4555c498b24aef9f813b5ec8f53d72781b385d5143c2b37f5d88195283edf759da76420921861ac85fd7c362b4aaaafa8022fe1d479a1596ae104f74d5720adf2d56c993370316ee35540d9af272d39c45f72c70e0ae1a7a33ab69e54b84a0b2a112d4c8f3904f3db063ec67f5bf9699d9d5d1ba1e6100103a5a655f3f22d13b015e66bcaeecfd84c85eec4bbeac862a9fc0e92143a4bb40f7b2299dae83e8f0287926adec35ffb2baf592d01ab720a4ee4aedbc08fbd7139a436d2a057eab27d4525e365a8e6452673f9d9932acea8349a4447febafc0ed262d3408eca80cf84db42cfe728f717bf56ed3abf2b568350d90755ac2fc06db721ab1b2703d1965cf3d24ca276bc691724c028135fff2483099a88f23f04a059d241db05aefd66bc1a0f9a5a90fea2414501a33a16c335bf7bdac5f1362fbe594af6e33fcc20ea4070cb9a5bf82cfaddbd37021be13646da7977f8fc0a7e0a290123eec3a012adeca28efa546dc3180e32571d8447c82f6c315720936fcfb775a44c772568bb403ff83271cba2241e04d763a08088e25e5e268d4aadee74058965627c9116bea47ee359a1e4a8fd5a030f7bd743d1d9eed07c89442107f5d50c58172cdc1a042742148441551610adb1b62059e547a6a98d536d2440d45a3b3bfb615486b00b3230a55151dfb467438abfc21f83e039645fd1464cecaaa029d46edf2f5ad6180028f04379d199af6df27229d704f4dd7313543b9680d124570cbd533561b8a3d7866d787ec072458e7cc244251eb5d830c95e464661085fb07c678998ab1a3684496a28f9b9b9ca113559f38b9877768d2127d4ec0877ff2ef7eba498a114dc08c41298ba03b55632daa0766c99c3ac5333a3d255d03d6eda65ba53f8ac3c85e71b28fae2b1ae419b279b7c15b872087682b3aee2fb717b0e0614d86e59a1851f9213c2a17df2bb84917cee977a46b87adf95394d0acd444f3e020a2c359a81432cb429c80fd2af79c26dc15982bd8e5dafe5fd1854b9e4ecafe55b00e10a70140f8cd4376f881ff050c2ad0e2a14cd8354ab825fb9b14caa4b78587d5388e4b92df590a93bac4d6c78a134b32ab86d1faea081b31626207f8f69bb85b1fe3e52fdfc2065ad71a633c35d700d1704147fcf3e911e1bb251fd8f1ac5289bc0169508f9a754b3698a04e284a30b4da0f5535e544eb8481a1e04a72e187e11f55713210ee5d4f6a825c69d0e907535592bb5728f1dd55b084c0dadfad63cff13de38e9558fa6f21f83ab540dcbd3f340ed15e6d0c94a9557ed35afd7f01cf51ad86e8041e5c3723637de011356274ffa5a52ccce6e40d3115eaafcc8f2ce837dbdc64b25aa96ff7d003705dc6dced78d3e1194421fe746c92f54b29a1201dad5ba09582e8c7dd486526ab1ac4958a5d1e507d41bd09462469a8a82501ed3f962ba72aba86f4675d5d56497114b38b456c346518f09cf75cfa55c4a802e4d69bbc3bfad52db556be08e85e994053a2054fb47a27ad4cf009770755f5961e05e81cb3b3798f8304a8e8a7921a99c71c341514dbebd1cd8d8ed118a0c141748cb03a47a6491f69d7628f51270adeddc9041f73bd15ee4b907e6cf849bb55a81207ff4fab7ef70048e729626fa42d18d8213d3fdcba251d8d54249836eb6ebb8ba3dd8d7473a0231c9cb150197dda1d523f7de26f7a44d0278cdaa273a5aaeea7e20d66937e665b2ca88d717eeb96656288edbb7da76795b48650de4e081f989ac69c02d207e683dd7acdc5f7660d58529cf502d4d47789f473db276b05e86935a813421347b492a6cf75c52cc9c92a87b27d7624ba7a458ab551da0746398b0090aad89b264fd4a4011d54235802ab6c76c018139b2a9a6d2f76271d648275a61f64af94ba3d13c5c9170cf6e5aa37a6c57e4060997c2bcf37ed13fcfd8b865d2cf2aadfa44ef6d82276d08b0d747dd53a23f4cd017d6e27d7b0308fffe399f5f233094be6d912315901cd0540572d687b8fe295c80d47c0960d1b98c7c7cffd2d67c6ba5e63125ad6066d7abf822135840a38b25389ac3a0af074b43c8c52bba1072f51afee1ff001e0c40e99b604a130ed4c9e1ed40e6ac21366fd270a0041e146871a278dfa007dad0b7563ef7fd69f8b6e612345a18c0f3cf24a4cf9fbde1b74c524f6099bbdaba3ab8c730725ac428bfd20b9915be913a73c1ad74e939e320b2b331ef459e822b3fd680fdb808624e6e9bfa70b77b8130b396904ee681536407b52f49bf6408b9e0a77d9bc61d7048873ece96431ab4a14fad53e582f18238135c1cffc610a6533006e03c12aee3e175db13fc035c556ed871e652a60d1bb2e4c87d17b2d68061e4f0a02f6c5c16594211607cf9161665ba5c40407c81948685038d78fbc1e2186e3b6b73851c86b2ffaa01bf936a038843abc0d46dc2b4c2e814cccece1361011cca16e0c2018f130d231b50b0b7401029a19ceb33939f68b36bb2eaa7be68a9aee43b9e5c2636b9b2afd6893eb815513b7cc0df962b86ef160f3132c16f5ac07370ab0a708c80cf112701c69aeda06b649d3912fdbe94c9af804d04ac21347c09b82896abf96b55099588c22c6660dcdf64aa352b5f15968596c2ee609e61836fc59f8d5195dea1704a5fd630c813ad2b9a9238c2a56714d2012a43d09e70c9133dbd10efbfe3524dbc808fc807dc8e90b9354f1c9a1c37b8423550088fd9bdc2a820dc8102e4a81d5b0101cea159f6fd71f3150f95554d0c0c716affbb2f6b2f107605ad28ede511f7a22ae3d5d6b3a5350f910d7a8587a02158ca82bdbd56c884bf17db425529c052d4d51276bf72c45fbff08e382671a0f70d720c72fbc62da73dce9f0a00260ba4e6ada5ac252906561b6a7dcc4c80e5c0be2c36c14125e2e7d53a64ca60301efc70342089a7adb387daf7d9656bfe74be16b3d872da893a734f0b54cc9c1fff39432ba7314e1bef81b233595124c106cadb8699dadaccdefe1e21e03652fb1cd5d3c14ba55563450a6f6276ad8803971674b959814ec088d8d5a34321df9d53295e12f923517c14f0048f435f4bea4dd268f0d9964ba8239f2a4f7c4aa9f874931e6339d10b0cc0e6e96d7aa9e277f1f0f54515662f46a0ed9579a754ec213cad6a6f77015d67d970517c3504f00cc7b3b24d4696432a50b4ca97fd8c64b0db94415bd657af168a281b82f84cfe27c4705683d98e859018c24060d6432897d0c0cfc221d4960b6c3935b70afe7dfc550cf854881320d719b680ad53d72698bab4e06d91de96f8292ea7dc3918c0697f0253f4cb0397efc7e25bfba23a78897f38fc8e0a296735f7ad87cad6d5907bbb5acae54c89650efd4c06a77f39c3e56c157721506af19c776ec929fc37a46ed9cfc13b7f915ff524d6fe6d8f29a898eebcb2ea99986a98d81d4c39d0cffdbceae9e6bd5fa09361379cfa4d7ddc94aa8d196491a70c4febac4bc6afcd0af8ff8be4b2489e560982f88158e0b66cdc9d4176c8e03a38a77ff1ffde05b23ba37e919575d4499af6229b0810070746d738de2c541798f3e8443ba8f83aaf75b827a338c7f702553c196ad9b26ac813ddae31b5f353382f1347c059609e15c9a98a6de6262a5d0ce05df81a9b2b569bb38140efe752986769a5e1e7a811b38ed8f99c1dcc620f0c55eca39c179fd6b2ff3e464aaa8f10ed716d113c41a543c6dc68cc92b51b69a2e57e451539dedfd083a45aa2d902ddb009865ff4fff3c93020308f8ded49ff1eee95fc4118bbefc3c4f636e4963290a38bd7bbf94828dcaa6d46384f476d0c353cc8db8e631d33d9866b9b2913f05a536ec568812815725dc632e06e7c995993e586da49f893f072d038f755b6ae43d6b7b9c26e5566244cda52f3088e0b528af782bccb7b84f3187181416803f943fae937e93903f65014d0ae9c7672cdd5b853839f83a001d5992ec0b9cf1a3d1eafb22e809a9786d2e6e072462ce624a06e5cc87eddbe604d0e4b27259ec076764350176b0661db72de48f0e7105ac05c04b6a9d96095cfae7a627f886fc0cd42bbecae4356cbe2c8a9c3debfea3ba89ed43b8462539c2074ce72941c6aeadf1337e880f91a4874e908dc18b6fba1000f5fbc01df16968233316bc9c42cd704ebc492d7a0c9662da3dc53df84ac0a541bc29d6c8252713b40708b584b890003f80b55146ab87e3349bece7de6c547c211a18d821b8eb9e332d96e810954e507099e9b402dc5d7b52260dd7c763b0cf6699a6052134fdf42c6a0fcd9741dd32afd9a6ffe6da180a4f1edb7efaf976673c284d52aa239fcb638ee48b9dcac5febf244df057accf9a1ef82f6e687ad0712501738c30b4925ab9fa5702bb046301db49386ed836a85e5d8688c11bef2b98b552a187ef7b145ece10fe46795e523733bc1adeaffc86eecbc5db8083d97951e23844994138b22f85a7020f1220a791472206d3c371e6eaa611f103f01d0115960214a090610036f76d9dceb37483a59de6f0b815860afaa79b1b4024b56df0bf32cf321f4a8c26a7ee4bd24a845f733efef2d0150d46fafbd4966d67acf41c19b023ee1766a75dd81517da0cfbcd86d2c9390ae740427830520bc2f5a24ac566123783155bc9f6a81c09ab8d666e1da4ee2f951b5b0f94ac839c0e7d3d28ce0df95f666436894447fd4c299bb0cf4aeb61fa7c82c6fad64f24c4b903a74bcdc119f41607a1889e9412c0424da80e8d469b8700acc0bc220da380801b6dc82786c519d5eafe4034d76185bfee4a9069f08f87d14c6d1bfc2206b052160ea6b9bc3784569370421be2fcf56c08cab80614d25dd95320d6fd3f001602366bf3668b0065725549c7ee98c7ff3fd073a6fba2e97fc2b0cc3112e052737c1d77a678894237d9257c7a1b4300430fabafbd51c42010903a1fd9072e5f656d004de051fa01a13d3f77cb15df2c17fea8a0b5a07f404f381f2462bc90489452fe1303258159037e5d754bcf1497fcf928dfdd94efa116dc38f57dd5a4638a523758b5126b5a8bdb8e0453910f884b118b101ef6388c73786a972b1befa78699e1883720d0cacb3fc51945f7fb0be491a6cb07107f0235f1fb10deb88f08042a8690142505c90e2daeecfae031334c0b59ce7d650d751ab6e86e3676ceaeb238aadfee530626ba626baa039c5cb67b616e6a738a498709dc04df27524b1fe011377d1e95a00b9f9f082fa12fd0c4e100115a87d6d22126c147218d3a4562b315234317219a297f0580a98e21550aec028aa19eccfdd319f61221222629d03ebe5cfe382824ec0024289d0871e1cc20e2483e110d3ef15cb702d18e8aebb08b451619569c5abed490cd1bac655ce1a3eff74800eb9d4bc87b5ac805353249b0fe19e3a6a307fbb2d1a8dcecab59ccc830454574cd6ad1e8045d8aa1ed7891f83a73621a995e4d727797bb25d07bf9c30190d20e3bad1b509a4fec65c59fc106224f1a7cdde2ba494e1d5fcca25c068bb44f41e69aa957af00a25e1a166e899a01eeb62e576d0a5baa8ab04fd3327820fb331f7e1c2bf401e6607a8c94863e510ce65407cbc8f81ad96e4cad7c4f50f95f916f7291ae0a00f38f997134528ac0d17819c1c7cfd1898b31070d615a9a2dbbf45d6131c6a39dad25bd822cf0cbf7b887399762e2ebf074ded6c592c9d20217b2cfbf21809b7750d86e32bc1a815b5e132e0b9ed73ef3d476a05306b59fade420bf77c851295392f727a1532c1efadadd914dfa0e5ecffe2a59057c552822e76a68a7a52649b5ac5ec69f07c5003805ce46c0e1523630e2142bcec834d7db79ebe6459d7dd6ba1cf87d7cc8fa5ab8eb0a59f4bc97b33ae89696c9a204683900c9b751c9444be87005c5228139c32ebc1ba5c201075f7bcb700d85fe383b8fc64c67fcf9de576a62dcb99c3a1085fe99272f18c20d9fb7aaf7314993f22b5eeeaa6ddb326e9033b8c12ee5b1862af5d32a316dd3bee9fde87b1101649ae6c00f859575ec4ade735807d37974754a4aa8936cb9879a380f12c6bc1f3957bcac94e8b61cd7fa491ff6070a4f32acf80a0ab7dc08b9a0a5690413a6f4205c9664e017777990bb1b19c22d801849a82642ecb1d801c6caffee13770c8ce4c976782da306bafee87e044eafb800e88ae47122bf57e1b1384ee78050527c927e77b580b6d182b30043731b623ed79ad630ad5970ab83b39657ec31e444b8f281969823cbdaf4fae7f6a1ef50be0dbdaf8a15c95a99b4ee031ffad1b157c3ddbadbac7449c77c8ebcf71a96c863c78e8bc7bfb9bd593d075355d02050739385ef5235d921baca9c9acebdb81053743b2150e1e7d45bc56c0083630ce3a02bc241f167e30ca9e0f22a2287b029dd6778e13d1b7c50ab0eed4ed7d90e3e80ca80f18009cc9b913e9e308f46e37a1003cd7a681b49564a160bb3293a1cfae8908b75cc04a791af00e996e54a5d262cbe33827bd5cf0c8fc4ed3f2c559872b24d518910fc1a5352f4b68e6e1841dbe5f76fef63902482454c3f4fefd00588b9ecff82ce49eaf4b5d3fadcdc3bd955aa1eef7284d87630e6e610252c5c98de7164500d77289b7c3cc41c08c820791426d591cbb8c4f856ee8823744d9a6080739bf7055ebfd26f45b9f5d935131c5515d69e99b9792dc09648fe23ec9cf9a2cd9475b23ee0ae440d4d16489f81b8ddcb5466a6dabb5424a5c85f810857b41f69713f9fdadd410ec7a30e716b8a909fec2fc84f0de3e6ee9e131cf1012a64e36b3e51a0ececc93a877987cfb32cd4bfbae54c080d10c54bb18d41525ca3c5bf009478d4824013d0158c8a8cd4bca680ae18cfcdd9e28d41fb87d55287bfcefe05e861103c3bf9666f35bd8b50d3e0bf65a9fb78b7f09d90b5d20f47b2d1e1d3e2840d5abd820dc846fc0fbd2fc168e521b188cecf65fcea9b5c0ca602fb89a521a98da9d23fe8a8d91fd294ef7b55a5d64db20a238e26c2be4e2c2765684b3125e20dec06dd8c746a8dbe974a283336fdd7310d23a6fc6d7cca3a67735b5a6d42ec1f144098b56528f8463b8e96af3d671c5ee5303a6e2dbdbd307fb836c996e84c0611423ab187422098c23a75c940d6db934b52b9359e2850f9bd962683ccff0ed39d0d48e3473a82b1490f3c9800f858e2e2b45efca7d849e2d5496faef80c3e38f0d39f9cbb1b8837d8628d28c7d0479e27058513e519ac5a0f30d7e931f1ac27a1b54fc4a760f1068918f0523084dd92cafac3e5c91d0fd4f2f1d358bc9cf803063ff0c37c1d36f21362ec8bd81af7853b804865775067c1fb99c9db8ec1a05b8a3f05d2c442ecc049a3cf177c9918b5232dfc640266cac479db947fc1fbe94966e1420b32d297d43ab4e98ad69f8ae64a90dac330b67759af00327c80e45426a51a8492d9dccbcec9989636299df60761d1f6a1ac9f4814af88490625a50887d23390b6bea46c8d9026b8aa2317f2bc920648d83ea70300a77bc249fa7159b244227dfcb4610e110708901afdd925e7fa1492a0f2fd6740291024eef058de9d947fb01ea45b9c92177fb40882f950aad85b729dd54393f015bf5e7ae5eec88a9972b125dd51afa3ce3c70268e0097e4ef734e30d40e0e7830d2ea54d041be0fa411b2b631e6f7f79654a8e5dd4b8e45ce257b14f572003c36dc0974248ad453e3a0256362ae625d0700fbd0555b23882b435b17ea00d9a0515cd808f818fffebf211882287d96d3bf47a45a484e471bfa6e6fb9ec21ee18c735686343ee40f8193649ed1e8df37fca8526fa56e6235b411105df239a98d7b47ce37bf46a53585e15a6eff0c02bf1a22750436502241e8262847cc2f2fbad108d4797341540c606f0b04e4fdb0eddf2b5c6b4bd519e9521cd89161cc5ffe38d0422aca64df7c74b6821abe929beb162859aed09a600cc489a11370c402ab6e9c809f920d0ca24740d03805db63ecb23371bc1e5f475074f982b2114381428589d2d51793e619809f2add79436a89a0861121cc5f774303a513d4a00ef720aab155d1f8820b44a5a2a19fbe5a5b02810732e0502b2fde5643e3fd2040efe54298579311c9df06d5291cd043a11a9710b7185f8c8ee208d3f56d5b14a0ceb5b9355e41a6d5080c7fdc2b17056c9d1b312d5c615a4dcf6c5c127c7c0fe700a7aabb00f9ebf318e070cf7557f749e19990b37127969a9c6e275ce3bbe558213dbb69740d1134e03e504e7c3fcff7cc7154ecb929116ac6b834aadd8c4a332eb0486f93b82d581f663c0f3e86cab3a4db6a14cc92f18b0b3c4602482fae79b826bda8e666c540ccc7819827495dc86957c26e53cfd76d3bdcdb989901d98e105784404778f7805023e1b604ba66cffa042b182f1d9b01f4eb9d88187850dc3cc1ad849e2751cd510eb0b8ac92795bd9bbc9298a9843023da3ba568a0a4d350ab17ecd1f3c99a35a5bd578a459b730ca080c157fa357190ddc648be04b0dba7c09b4ebc6c5f6b97e89b36183200c52cdd1d5a90a86929b905184aa4970953cbefd25f0b4fae68da87cec39846e7c656d5fe422d5179044e4a4a62cb73106b0274cc04dd8ad5aef15830fd9e7b962fd226069a54b42e6a4b86360d6399cf6777dc2ed62140447b4893e04f79031f201be415c6d983d6709b06f8aa692485751edb6abbd1e4090808d97f4428c66611d09394505220cc12f712bb1947687fc68856643e9a0730910202c055ff0f92013a3852f2289db8f823c91b3811b8c334e1ba7ba44548ed5e2c93c579f55c2729ce1d842c636440c029e20008b7b82f791c09833dc1714d6c80643cd272796169cdafa8adf75835c76af605ee93e50c58cd25af887f146ac5e27156e18111c497c3da965d49ea82f811aa4cb08359292231032f34bf10cf8e67c66423a5431b75729f73cb0146f66df55e3925ce81e3539d18911589918881a688a4b8cb929d60241477d81a03eeb03f61181526442c10d0b9362f217a4a4342d2c84f095aec2be120e7298d82238808061b85872fd367d1c5ae9b54eac22270fc643abae53ff8e47f9f9b173b603c9fdb62cf0de8a8f80ede2f1d1e18d8b01a83380b80624391dc3b6396ef5851e2e033f69e6d42829a86198f54b36cbced2c9d265d0fa19410f38733519ceeb6d662831055440bb00d3e4788c72b3a003f5eb2e474a2f951aa3ced0df0e2ca93ebc855fd3b879ab53a05f923f211030febe2e2a4f58eb2b1ef6771c32a17dc711484650db307813c9e5232eeb5cd1fd11af365ccaa816c71ad3b1bae6f8c7681d884adb2902a0d86544060ad667c1edc2d836268c0fcb1561f2ae72415a952a475f10602774af14fd9a2f355f145ce300b7fa05642efbb5fd75c9f45b01e392b71432399f77cb0d01b5045b1c0f6e9edc273bca6186cdb681aec1a9fa86fffb2e40d2864b3b99af7f692ee82cd11f9bc1cbb3abe65cb1039aef922ab01b9df410d7447226a59db4c59d6f52cb6a37da736a501a30ee77509a14b6ab578f7ace56bd533838f5319d676bea1650fcc1c1c5742e1155e4210d99b944e9927d81be1f3387e43ff3f2f1f84f2a2c3fb1dec1cedd9c5e2dc90c00333fca4334b251616714627912b2876c5d5fd758f46a88fb1768a2ef1ac49c5d7c729a7d0a9cbe2700c9f4f9e27fbdcd7efa9c6159313560714e446f737da40297de6e57da22c40f36e62c04ce2332cd0dec1b1e9e8b17fd1ae4bc09957b2702d6f7eae3aa17bea307620a09592427e87a4d85170e80e4aa39d5cbcfc53aabfc40f1251171fdf1e0e53bc02812e98968d65ad6e10b105870134a3ccd089191b9b372081780ddba30575041788ae77479a64b640ffed06b15d4730a6f09f6a9c17cb862ccf9bfed2a3b961e44983c26a3a047fba6b9e7eb50d40b2f12e0efe3926d224bc7a8e6ef4f68fe1ca3f89ab5a596787c5b1e852689372e0962ba16c23b6a61428f0bd80fbcf2227dc8b57a991c306ae17dfef6207562fce5af0464dfac1665582934e9fe35aaf174d67e9fce35ea7f85629f23da83d97b6b3212d98745e6a6d800653b217d59048e1032a1c0d8fb7ba6dc774d82ae150fabb5251389d0d36e5de478feabfa2a4f7ac36420f3dab96e378803ad0fb345b33f946dd8f81df6a2c4f0bdbd89bbaf08fd657a3c432bdc4bc7149fc6ea9b7cb3c207edcb73af89cd686065b8142139c4573f95b666ded54ddd82adb7cb974fbfd5ee81acdf7171bad26d9291fc1d08e330474d86600702182fd1bc10ed7c3fdc13afae60f03b1c0d67853f5fd1acc1fa01f1b7fca2af00f8ecd7402bc02959b85634b9bde34d57eb74d02640582707e06834847b24682c9d7efa60c1217d366c3ff888448eab178e9efec05c7c71a06764ad6018f2120a503fb89ce59520a7250d94fb549db63acfe48d6a1988aec1f4875c329f59806434f50c918b817584313e469a0d3f354d40010f318e41324b21c89fdccf502c836d66c9405e67901fb09c1af11b6e115d0fca6f33bdc44b0e7881532fec8f55e072c2c3999cf5692ed0892e48d402d4a7632f68cd8f4678ce49c8e6233483cd95c5067223722043d682b3a857c1a3c309bbc1fd7bbf8066404c05fd4682104fbffb4fb58732ff92124a62f48a73627c2a2167bde8ac4533e772326f008986de981559689195765d4b4b0d1230c1edcf3771ac7cb60bca0b5b751b4c67777881d5b88bcf2baf41be490636d496ebc6c1719e440fefe82bbbb4195e894b770a405ca59347ae07356039bb53afce0aacca02f12698a72c822917b9c6ccb011810278b05e5e053874e6d59aa9796d405ec76abf48ea3f6f2d6f2f1e457887af8936e9678920b43121031099c7a909ae7836ad9eb99a1379c304bfe87be4d4af3a2316eb112ec95dab51edfcab95d79ac192bd18cef3068e110b05d77109efbe233fbce86633c013cbc3820628fd44f894f4df131b80bc7dbd4bcae68beffa1acc6a01021c45408675339b3649ceef51a03cfb5e03f7f051ec762964825bf124c9a468aba9125696138168a9cdcfecd8b77a7186a89d6581ee1fb703920162aeb55d8880f6f333add04dbaa2c65dabf6933afcca3fb7e0f207e6afa54a80a84d6886f0beb39cf1ac55c8c66a70fdcbbc1072d20ba8ab1ba2b07eb113ef7c451f432848d14fd29ec95c6c7c5afc7c6bcec3c71f2c3c6a04ecbab98499d9c602ad55346b31cc160a70c2631e48d5f1d9a32a9c98dfe5e399f0e46c59bb2af8dd1fe0fe56495e68efde2e58c26a511ad30a67c362c5fbef674cd5a13586bb635c323411700196933bede2f6f3f79d0271bd364b3f45ae01ad4c75c69291f0305111a139290dff4618e525cee521b1e6b256b7af7e0ddf82bda55a4b909f1ee06cca0f29fd62b948fbfe955ffcaa1768ed8230ee878608c105f3222e33776d3358f2b95fb5117357efcc8345c19c7e67f69003b7d762873079a011a2eb3c9fce8bbf58f3de5c9799ba904b957d04bbeb27a4d12b84c188a672be3321fc9a9a83afdb7ed1bafa58832284899aba22bb015969c2cddc11d4465e7f0b18311e0bfa18e186f8a43580c48be3c7deb548382fed4a54aef084292932c27bf81892e7abb428704fa86ebbb5ff828f4892e186ba52962a6d93986385ef13339adf1b248139597b809a093758fe2a4850c84dc1ae1368497e9911aef6efbbab7ed634786be21242299b4ea10b56114c7dadddf959f6417e649a867d35b4cd8a672400a04e72853fa4fdd3e033cb60f44d4533ae51f5bf9cb9090889aedd8da1585120d1f8e607f973e701e880077c08a645f9ce44228c0bed606ebd5d40210d0601d87ab4b32645c3c7e32d4b21c8498c4c61b02440709f0fb0126ef8da4204af4aebbb3eb63440352add18be30935b83ec88010222a9dfaf86ad94b7c8777ebc78942a51a02067fa0c41beb53dc66155dd4cfc88655ce61f9dc64077e59f0f6a80f4430119f909a5fed562c0d78dcb9f441236c55d5d5cf4d33454972f26794132fdb25ae9312fd0d71cf12b45afd1d850816bf8ce235b33997471fa305cc75bf6741708a603e5441d214b12feab04dc69baf31d4821c38d006fc33a683b3c4b3242438a6c19acd145eaa99e0bc3295f63fc02e82c15ed90635aa3a830d0b34fd8c1d5cc009155af6c196c4d74b3da2763ec757e636d2334f56d4291ba6e7f28c69c19f42561cd9953e37909940c8022fc8d3f7cd6e49716b9032859866b379929acf9f9c83f5a8b4fff0da5e905a062a1f70bc4d27d543c55ab7b3e5e010f15991ff8bfad8b06f3c8fdcef3bcf264f4a85f80031f8460ac65a60e9a2a103170bdf7a64b6eea3ad751f37549e35599d7b2181c4321542ec2515d04d3286045562d8f9fc54b784090cd6eb28eaed85c9335a4928709a4c2b5bd664d34f5312219fc2238b5735bfd6db9ca86c7651107ed0de2aec37d2da3a602b4751bb3868dfb6ef8004b87e1fdfa51253321edb2e992f3a5752acbf236c058dd36ab6adbfd619e58fb307a1aba952b489a5b611bce65abb7775497fc8d040ff3a7d0f5d08718cb0de87a4106520ae3932e27be863be5e26a5beb9d4bcc8a060b93397d9e1fc7b78e2b09cb9a495f9510856f72e540db23544f3a676576ae5521b421f856ab4d94635bd104ce2748381233d44a26b8bfa6c08955eaf4f3b213cfc598a345872cd25714a266220ec3dfa3eab9b4be305e3f8908799ac3dfe9f827ad5cdbe9ef54adc1af214b2f112f090936f1a385c24fb4c3f63b8f17e72426e2cab312c2ea990fc8378e3995439789127a891154dce820e8d2d562aa76070f7841f04f8ce931064a099ee127b3ab5345dee3243e4c5a0902e3d1206f157bbd68ad6bbda4e9e7478440aedb2293cfbdf9b90927f365b64387abac8628a1d0ec98dfaee91af3aa53c04e30f8dd77c1c980dd702ee56146403aa6310adb8edba498f6e8e547e2392044a62d34836b0ec04e0149a7f84df850195550f3e8c7aae538292850eed163088510800022dd57ebfca51716086ec37aa2e9d2af568166c26b82739669e16a97bfb54877d354d1c2aeb984f069ee983461c4702c15e51ef1505942f8d92eb86ebe0f80c989f79484281787caf571b5bb5c65b76f3e5b96d625c1723457b081a53a511e4d0c5b3a47854c2c07275bb559a8567c08255243f7430d983cd673291c27c7494fe701b07142cd1581fa49d040990b01a145ee6beb25b9b68f3a41ad3f628a72036686910ffc393f2f4543bdb484507cfa27dc7411641d1e21ce8003ddce4a36d41449a6a83fc193479902e2ca4d4027eadbb7ccd08204f11b5d205e805905ac5b3a2c989999cb5b68125dad6b4e6aa7c66fee304385c134dbc06c6115884fc246dab4dd637f47b85828ed2237a9b9e49cbe942f952ca3863cfea2176e4230ed78eb85aeef869e138666ce01a55f4bce9eb36986b735261edf1d7d459d54f47d9d9d3354b6656a22be7e682afc32bb74e443dbd88153dd4b02b3ed80749ddfdaae65a0cf4229f2e650ac8a51fa95ec2515c70543e8125441fc20e5950e8a326806ef2335931c012d6ba62bda8afd312b149c3eccf27e75859ffb73c92895c03c0dad96f8784273b69c5e4bb51a720583cb410c0c6fb00303c022c908cc7259299d1e25ee21b8015748216e84fd0edf7b688a489827fc07198e7e62e78d42a583298fdb6d76a46a84caf88519407ed903ed5ade4c705f088e53cf20d7b4f5352eacaf5fe301ec296bde8a513771d1ca9d8ac91b4251828a389b56e854b0223f98ad4994517cdcdd16c6ead6e0221f5fc8ad1827753ad38eb894682f8167b61e2cc082b389e3fcb69ae9ee7bf5a2ba71b9688b2165ca10786dc13025296504f1308757f65f9efbac6031a07fba3efd08b88d205e6103b856f065384d145d0be533b512c0061774a75bea48378b3da810033e347bb926e2c9800dbe041f186c546b3565402f42dd9051b513b8c6f6ab623b0475fc9a59ccb9a9ca1d3121f83530c6f5a22db7a67bf114c86eae1668f6a19267352d370742fbb3656a8ebf8f5061bf34663a66ac56f44de17e62d44ee8931edfbccc3d20df603d2514508ba093b43f2f51e4ba01f029a82e1f7afd08a3022a54cbc26c061c24717aabd815e223341d348db0a433e2ec0c5f769c98908b5d0b6582a3122cb63313e16e13c902cf140f35df8db538732d40a8e356373af9bbcd04a2a56a53617b1c048fbfc7f3f7c0f8499a58cd815c0870b4b1a3ed2354575042a954db84cdf058037fc13c303cca3db5a6a9d7d63274590ee19f96adb1b78ade1e3005e4c55378851721ce8f80b112393327c181fbfd3d0eab86b8e9b7cea2ec20d8c8fb564d6e033b4eca5f919de33a033cec73a60660d3d557e2517e38cdfb45251813d67fdc808ff10b9fb89c7bd953d65d71e5254481560f8aa6292ca39bd87e7a3f9f34197ea513076d1a0f3695752fce3cdcc67aa643985c02fbc23325772dcbfb59e47067e831f7865e8703b3056cfd59470af91d5e71113ec1fb8269cdea50c19f0d92b589c4f4a3516666bb12905bf70439ea141c1c8861e4415a9e9fcf2e735c8400012c0c6705e605e174905f9c71f74308ff9c0418122e06ae764b9c581e855e16c757a43b652ee46443ddc07914224859db845fcdde0f228f44ee5daa58050d0443c244d9f2330b21788003cf5bc997b4c3f01641d79a6a5e1567e6ad5de2e787ee5a19dd6eaf145714520846312bfbd34fed125022ef76258a1f76c7780a88f977f888f00d808bcf3b63e5de4783e8094e72abe009be102cf10ac457ff0c87558a78f23929079a031f4e90e98051c67b6d410aef8bc344ff19162a85668df9be4133004d84399377df4d3010db6c637e5ab8a67638ceab8559bcbe635fd27b647dbb3792a033f78925ba586ce7740fa5ad4ba6ccba490c0385082520ec6c76fd84fb68c0bec2e9cebdfc78cd10751559e755bd9dce5f90eb6c28c75182eacee7460bbcc4b4e160c8644d8320717a8953c962bf61225e39db6bf63a4102204e150405a225b444fe02bebb78994fe46ccd7e5905aff4a97fd76bc1d0c8a5a7e70dd0ea74fbc9685eba16d9a111850631fa0ae222a6b207dbda6b5957c2da17739ac06d701d570bcc3465325687374497707e5cb5107448a2ac1f92a5ddec229d8b0d964f4ff93b9f9419a9bcb5119f496eb8d6dcb76e0a026d86819a115ed37fb456a8aecc623448869eda3c7e64753edf8943aef472a2039dd92bd9c645a05ba317dbd38643c1849fb7e6e61da0c0545506d8096a005b639a888184a1eb5f28a5ac19a238206862c47f8c32fd592ef8663e4219ba1d39a2d6b109b93b30c0beca1415dc08889a9338f13b9eacd8ce7ceb3628ec93fe8f45b0160ec902cc0a633e46f4dcc582add886d78020faa97211b7e1537d6497ecc04405f0714be4466db9820cfceba3cc5dbf010b17cd4ceb90896a33fa13f12a120da923602ac119092deb93abaa27fca638f6c38ffdc65bdb8013870b0a4d5b4e700333012967964e5af3da01dcfa1a77425591d55bbf2b1b310c895767c4226056d88c3b39a94c4e7b7c7580e3dcb82af33aca22d89fc4338ea9c426ba96bb7ab1642023bcfb11b96ec17fb5a89a04d5884325480f3804e117e3b0094184aa68d36e3c2c3fa7ed76d40d02bbd9c545955e2e4a9f12814d2d48e43f4a2d05359917c09cc529f364deb4beea499879caad762275e7533c7490c09e8674700eefc36f5a90d436345ccd2d78c31e922fba92abdf0a22c81d9ac1242781530987d11c4d860a3f1c1eab02606d2de89cf69d6160f38590a9ad215a720db468a818ce28d72627a32e8c0cc984bba8832b8a57a141330572917fdb0df09a4b1ff227141cf94bd294d7f8e82aca8f8a252028d504cca0d8645956c348f6fcbc1ddaaaf6043afac2f4a070351cdd635fbd79ca604c9a6f0fa28d7404f4bc799569771360c5004a492ae52f212720cb7741cc03af10c8660a80fc540eebb0b3943ee687a2999f209c8fe2be507aa238fcf98191ee51af221ec0cdb228046520aa299e2343e8f9c7008b652e95b1db26e28dc8e86ea97fda25753a32ab7239b9e4804fb3bc63d6da854907ce6d38fb6a86faeb2657dd970f9843adc6f2d032daeed9161e786054b7673b8ed914e3f5d6bc4292bbdf4ac18ccf6eef0f628a1b2acdd7323172246b12a3c7107a04f6bb1d180fe3f8f665c99b1621981972065c764f2405bfc1949852664bc323915f1a8a91d0f89931d7883520a85e768b1cbfd7bead7f180696cbb4d1d7f32f7296b346ada5256796eec5daa996a961565bb7dad37172f8fed449c281c57cd0407f715ef83d438c6688cb41d5b55e9c8ea6dbfd032e2e0818c3beaa69a4273e378409c3a53b144f52a4ae54a5dcd0aacbb46397af9caf0d9331234c326fde59c9c3fc8226c6d716808297ab2b9b6eb3af70858d68ae77c543548e8f56a473cd9ce3f73ac4ab0baa8d4043f0e9a41c8d12f9cbb39276dce4023045313babf254d0bb562901e0c021e387020ba229e5efb0379911be08912a4353f69fc254d68f987755fd113199976a8db4c8777f99eafd09d575a8221156b66ed288eebeae4cbf0151e8daccbdaafab67dce16d027b817cd82536673fb2a7c196b068e57c9b74ef176004b592755bb29cc25cc09052a3fe541ab2218fcc40621cef16e46eeb3302f8c3a4ec59e82a3a7d89e4fe40dd0f6a1391c8bbfcbc331dd5a8c1515c0df11d83d5ded56b324bb503654359956a19fbb84ad5458699d305c574b6ede7467127f2906a1baa9efc35cacaab0a457bc6082808d37dd4cc09b8911b48f426917760e1f41a211b7cbad1354420547e5ae833a757f6982ebf5024c529ca175d15ebbe7b8d40fdd67797c05e7e738d3046b6782c8c77ec0b86ffd32db63248eedd7c8bef61c69f25f04ad86d7f0565fc51426402beb741435af609164ed294efcf1c7ea12b901bf89b34d9e915a42afbafa451bb76b047de9f834b9f203011123d5a8362af638caa89c30d5fd4adfc54d2e005d6472edd08e8893d7b3655bf95a51401a2c1164bae95ddb7ec680ba388bff0836a832b1e24491b08da92fba86a854504b200d3d0fa66e4dde006c1e67e0f24f55dbd49ba7bc600301d3b923e53b81a4b8802a2bd780814bc706a51fbc4b965bea5eb97b7e3670b0bc3dd94ab0db0003a1c0de4f37d995606edf659353be173095e1241848a4f2378b328c54805425d630b30197ea89ae4a44b9df5c6131c173c83b3820d38551c724e4ba8b0010354e016ebdca1b800e91635d9a9c0eee91cc3cd450e6207f9a731d1050b4d004aad5e3b3bdbb00eb8094d385b2a7bf282f4342be7c38b39e2739a14957be6defb0dc6382d6be399bd629905c7bedeeab7462afb34d3da9f4f66f71f2a24b38955414f8ac2b28cbde8e74cdee51157e9c818d71439a764180d6dcbb5f84029f5e0ea3bf85ebeeb6b67fe439e3b86bf080171e999ef2c7e50997c1781e4a894a758c3e68ba902aafcabb0dac0e102a8a573060b2369a8106c02cef8af2a1607e8669c58a7ee0712f731453225c7bcd9f6199d2d51feced393058588579913f70d54af7b58c81105cfc15206abe586bd56f93c80f576e45634c141810d0bbd70c4496dc5605f143d9eb14fc98a3f227229265083da14a825012ab2e56131e84420246f76c60d702a8661efde435b8d1ab076457251aab648b364b0c87acbe513b38f99da8dcc6af5baf67a030c04f560246b27b2821711eab8f0f98eeab14cacda641589054557bbb6ef9de89e2ab1ba99676a52e408b4df0f66784fe3cb009ef92a7fbc617ed6f4ec055e92c4372ad0386d3414651c7542dbc362129d499558f3983f7ccc0119759561cbc3402ed61cc7697cd216125f8c3669c3dbdddc6d426c16304e1850e10789888119faaa54c5b84a4868e5ab31108e64c57c3cbeaa5e7f1cf8f75950400990d5d5f15544237bdf0eb16edf6b1a52d9c6d1c28dd3d2df4772d117de73f74be7f60eb9dc123ab137884f77210a930657e7539c01670ae99aeb082b0afb09cabee94e0431e12f73a10e542021780786f18814c58896923cc5360a608cc5a8899d1b915dd9cc4834692b1c2b98d3c55875ed88f39705f58235c2f470d1400914f9cbc18022f4624897962d315af3d1c7cac81328e0675a2145a657f3f3b1f01ae6e825156d9cb0022c54d2fb18d282788004cf0d8b03841df6788080f306743835e18c55c0d21ed5be661c490002f1e6a60739d0071c3ed6a12d442c9fceefde0df404b4481bf848eea826dc8c0dc150093bf29a3fa33cf8fbc0c9661ee24ae7fa9fb3ee11ba86a0d7b88cc1ef0871905f1f905ec39c73ae0becc1d87f599c3514b019e2552f1e285f315c7709adddebaddeb56c17265ad2157bcb2827969b5ba4aae04a8ca3a9130a6d82a371874afbcb64d7e44f665bb35a600a79a8f072e8b2015557eb41950f2c9ce478fec84f08d47bdedfc97f043dc0f23defa7ac2d0850810b915f8b7cf70341f2bda6ede9200c3063789e0ac593ea926427d8361d8729d856b3703a211dbdc09f458022b41340243ce22fab2c27601134cf94bf08eb3f808e7e973fde82a094043d71ae92ab53929f7d6700de898ae8cb6b950677a5658cb78d0f96b81c913617fefbedd5b280ef3378bb3bca71d39b9101f362e31c143f4ae159c8761ba7532a6b300e37593bb7d2dccfa6c1108e75ab5c0aadd23088d52b32ee0c178548f8686dba22fb9ef18efc4f38a8c6d78da960f63e4c10879a9d72013f440f0ef3cd5b2cdcfd65b2e031c16fa3658cdad51b217c62556ab1cfdf9aa9ca049aba5c92ee00e3557ceaccf85deeb65831fb46e109f28ad4d232f1972c27136521527f61d748052c342da826d1201fb0e5ffa77c2a68b1dcbd4bd3618eaca9989c536afa6ce42ae55e2b7357e9b54f321f886466dcf8d3701420012533d1dbdcd570ac6c0fb6cbb99bd409731c73d2374aee0a0767e9f91318b17f8484c312777501c4c8aefe23eaaf50e6bb1d91749f4b75fd9bb2e00f112fae9ac0502ee0d572ceaa6ce63876d002fa825c5cf0a51739d88656c027dfefce8c1cc054e08b3222f0d4bf0650f4b342366e5fa0046fc1fe1dbd418bb958b1c9ccbdb0942008d49422713676e16311e554002ce236d4d1a370168c7f19d337f37f9ef1990381c0809d668757e65b9f083ca8fb31c27d19d0ce754cbec0367f3d2ebe2057902873b438c9867620ff45be962546112e0d46f4f4b869000e2d2da2164d85ada38ed46bfb596f7c6f72083c86b9d8a70a019c599ae9a45e754d1820e85160c80934475e9823a2a333c0c81565eb26ad401d3d7bd0034560590789cd424e7e7917b7864493da7d19e5f4b45845295044db1644022bbc99bd3fd7ec08627095f77ccbfaea6edafc4a49b921b84693f1f72e1bfc011758950b0e2645c12c880237262ab368f404f083ddbb57575396eb1cffdf3d2cbe908362753a69e66cd6a622f6603c83b3382bf645f11a35290375010b2c871d218ee97e78a2101ae5a000ae9fac00e286fedb98e1358ec891ecc24d79fa8fef41a79fcd1ff3837b3500013e14e41585579873a0430ef6341ed50d042f76e7219161034bf16bc49e9c6252ebfd7c5014e0e3d20e5b3461a0ec4d6d356afcd82d38576a6534f78261c3f6504f44a58e3e5c42b96c30af9add47b17c837d92338f67e4089788a173566f82532044431d29067af855499b98bdfc0efa955a068fc3c8834d3174e76aefeaaf30cce8c9cf33bb5f714060d37e1b18d3007268999fa31f4a46ad5ab5516e782892365547478886f8dc27a10a1cf7a7103ce37fcb0893ef30ccc8932d0a85a2724fb00a2fb35b423883c9a5af63ac0644c8cacaeb1ed02a42eccfb8d49c42cce068c5875122e5f1bc8831518eec6bd40c20a882f4fe3422f69bade014af53b060527bb25e976b51cef9ebabeed8779e8c946fb05cfe2a7e58dca74f0f160ebcb4581e6c19db9c159eac1c0712bcc8daad5a21fd69cb5a5961949695de93b4afe671326ce3980b584808f49acb6e5a2e62e1f3c7fc52638dfb1fbfed0202c6439d727cc36b343804516f401e39dd35bf1ea4ce4b0ac56ad006d2b066cbe17e7d076450cffcd5a8caefe0e52426d041c7fbd1830807255730e6a60467da5dd31fa465625df45dea4bbbac3e94d870e9c3f682b5a40af85d959531858182c6f5bfd32cc9bd73bb06abad652c2fa04d723b24b46002511b51e42f52a35248790b82f182e1f4042709e0f164c2f0d978a19d694496a59743278b84fc9175e79cbd017d8e9818b3c424b125fd8981806aeed064636979b0a70f81750b514235442c77ff592bbf8251ab76f3e0548dcdd95e5501d5687d9980ecd89a69f2973c26808a22106822a404d437ae973f79b50a60a1aa024b6316d311bc0b348c193287f52c1daf6fa5dc81423388998dbe51462903a8a26db331f713cb701776d7435da75679bc5b5e97f25eb226f0d4c772f61fc8654516ee61c2a61d5ba61d9b9a394a71136e16bfd73517bc239b318936bb4839c103bcc3b313ffcf7775e8edf991b6db17e597b3ad9057929ed4a2e6b1b00fb3d9f000e4512ee62ba56a60b548f45bc54803704d974795d5e1c3d8b4ab61b111020ce7bb30caa990038ac8bb694b6294ad9c061de4ec73e1e228cbc0c0e54e4e67f83fc0216364eb6562a8880455fa5c119984cf98ddf21b02cc0591b605f2ada414109d6f3c330b2e2a515b8cf8fe635365c74f702d83154d6c58de0f3325fa7564287f21e8e262e91d98035c740b8c88b7f08b24970205d8ab6f524c4ad4c18ad77f51d1f63eb63a57fc2dd52373a5320215dd1a7823de074842b390c74480e53d7070c09b3438652e1931b6b338bc07c46a0c8e00ace43220c3daa4d06f44c48effb19e30b09f3eea5bc629a54fa48ecc97f8c9cb7d6b43c90ad2f884078130a4fe34de3327c37ded370f567bddf7f269b60146a094a7a766d5655801a84a036bde10bf05cf0520a81ff7653bd1848634ef5c172df08349798bf77a8ce58637dfde8c1b4e3029738361f0c310e2ae358adb8a02743b4b09598fb940654f8139a17dfc9d4db796cef32bb1a23253d70f99413b9af3263beb69325a96e512c2bb7d1d8eeec7938b939a54f0415e1cf1b5ffd9ed7e798f26b8ef3c50f6106c541fb3c5ced880ed1deedf5ebebb48357c77e3e8b9c409ebf1a1ebbbc771c2907df0284aac4e59de91b3cbd5011b8410984c4816ce51db92bbea1db4cb59f22f3c820fe155c25edf2d3c73bf1f60d37284e76fdfbf605dc0bd312b62b217f67c1dfbd26124e7032473c39ae90848f2788009aaf7390747badb3462c60fc05a6454b40135e23bd81c1d16242d2e577d65a262b4cf52da9b4bca3a5ef6644e37b8beb5d7a38d36e4d80a1a92cf5fd7e19058c794474556e650dd58459230f918d5eb8bdc6ea62055e1d82bca5e5df8ba5f631113bbc6e4613f1ad46c62f110fb78e98f704e102592ac3b7a64a92fd199cc8d1a5444df458e8a1ba7f58a2683cb120486fd4ccf669cf292e271e2c889044d4abe26552c2e83b0edc10cae3cc9dd7f15efda6bd94d221652941781c65af0a85c2d615d79744da38faf91564bc0d56bc6c1150b24355d4fbd71537012a256335ac735a796bfa4ed41f046ebd6073ffd6953443a0d9079528bc5097725d00eb09260a9e73f8f3863f1e6d88717354590f773836ce47c8311039735a2baa2a205dc1762fc285f5ef6ea837a730a9f405e5c46b35a32bbae3f60d0a0bb74e205e143e374911cce25ecfce596630e9685074e8459d5fa435b4eee92beef5932eae9e7fefa770c9429f6a6e531aa311371ee39b3bbc7e7a56e028ac8e3d07797622224af27a42fa546d8e3a738032a4067c9993cde74bb27e4b9efca5d4136d3f1dfec189fe723bc27e6d828983a72e4eda616296d7c8aab3dec4e7c22b3fd6aab7480c58f074022c5317209a05d5cd0c2fa35e48ea96be9d450a119950c400b00f8d87ac6a72a775e0bd5842ad8f21763fef66ac35fa16763a2db310146e41f2b0667b55f34911e0cf1a8d5920e82435309884269b286f32936e8a90984d0051946c1edb7c1b442b7ee5e0dd7cb5d9f3d4710c1f71d4b96364e23c430f4608063c1f65e4fbe9cb1b01485d104785923e12bdd89b7e102c76df3907a29fa5f79840bead46553b8dbaab975b73d1ef802f9bd712f54a8fd3445a19837e35c655a882d96765db547f9bd275bd8b4336822662ec16405f3fa3c1e0d2e7f0d3fccac8c55a19f0eb488531e600e1982d22145fe4ebfd26a1c1dabff67b5b3514a1600da0dcb2c671ed823fe829f72a787d0219f0e65483970e94f204c2264cd4b6f3c89040abb15728ccf4fb4ecb37205d01568f7949f42ec878669909ef96ae7cba1530d55f4b1b04b6dcb6684e0b83709db6d61f242922ff2be865770c3587fe1201dcab3be413c27f84e3d5c1b01a52dc2d1d8039190a178acd0deef5737358a9929359b205f0aeb06b55811e85dde84fc061cc0a01169cd42fa8f2ba42f32e774f426928ef477c44e1907505e5e40bcdb900520229fb6238f956ec0e0de46839ee77ef04e691c23118484afcbdd638710bceb83e1dd11e325a0b27b6aa80760fe18033464a39f96b046292ea3ea5237edec79df17a1b9c13f7d54d72753f0d91767a0ba844fc4a465320ad4403c182e5738635084437a2272490dccfcca95e843c1ec3e697f39ccfe0d3d50c9873198dcb2a3e1755083b9a5c3077d14fb004c8c22c6affd2259bfb46e44c910e624411dce37b5c7dfe6392f0b9515e2fc27b8a314e7dde9ba0c4b58dd9f05dab6a74227e0a6619572af83bc4d75c5d688348759dd062ffc9153f6e2b58e99fca8b6c6d2a590dd9ae1894a701b5b3f9f54c1237263934a5bf2e896f77e35eb190835a7e196fbe57901a30f54ad4fc7b0f1ad73eef4510b8e82a5d756c5e99d84ba95053e81c9bc8d75c0297bbc48779d699007bc06f1b1c105ed0705729e08d5a72f06be91db02ff9121b984ce301bb9be3b0168960a085859ef86b5bf31af66e5e394bafb3ac93afc4551b6b50adaf5acf63c88d805a62ce84fda33669baf11ff17a6d7c3929611f3b3fb4848ef6fdc2e9d762f073b896bd7c2aae0760fe7a0defdd94b44c94a677da2cdc6bcc10088b48b0b147cd388357f5ed34642b3a4d26e1eba1d5ac791a61865fe36a29f2b2f718ca0da13646edcbe0764311b01acf095e83329da17e131187c55d08c85dbed929bebc41803a046a6e03d8f4740b1e02448eff86a56a3536eafd5ceabbb8efef610d323751bf0e7e0e282c8b78a10b030a869955ca86622c36f19f80e4fc0725d6e894b8c64e69547914ebe5736aebc779b204581a7835f2632c07df96004c1acb3bf095e95f5be0965773ba86065df864105eb97f82576e23f407091a1da36c539551f3e25557db77774b2620b99a64e39f254b69da03ea93da89675cf29b72edcfe643f02979d9e838a62bf88667c5549c2fd9a8a98063901434e0cbf1d2570c2f2d61a58f267ee58c97776540ebe80c50f512795e94a0581cb65780bd5e6ee2bfb31e382f0ac52126aa057d06c56613dd5d2d3be0094b946055501f58f15982c6bcf24dd4eaae1b9b20e3fb628f0aa80494d2f2220bbf18bb693e181c26405b2300582b8da2614cfe33a58d44f415ec1b6b859378369a774437d15aa954d21abe0a28d33bb7b40110d74163e8892eb94263bb59271aad4e81b58177a91cd3fce51a64605700861360896f3a1234cc5bb95c38d40ddc6fcc8e788dafe1101743d66208f4386608e816090101e2faf87c8b75b253a78e1eb19ec346f43170da31033b185e427a55b9237cd99d0c900890fa171049c95a3c2d4fb363718de2ea1440acd877ee0206628d50eaa60326620097a30467202c060013ac8d363753745dcdd738f90fa6829d3721d7f850274553af4fe7f7e648f03a03800b6f80fcb00ba2d796d9e0335805dfdc0d749fb8495b4ce5136843b9e157560136feb5eacb89ced1899b6a83ce6b27b4af088929afd446fdeabd3db81b3ce122d1ef463c16b9e10a40ceee5c213b27889762c37c383cffe60977711757fb6d54b5f32e6fc3d7f7cfc86f831bc59a50165e76c0ea898db3fa69b0bc851c42ffa8995126026eb859beb80c78883c2e07531b53c31d9ec846c79c7b8f086e1fc2210ae8f26a4c8d90d166711fe9d6398b77059637af17b41d2da85a93e84e86cd38fd4e1944421ae07e2ee15178d9d065046b144e74be8c68b9ac0bf05cb86080bec5d4e24d37de46578887e16316403c11f703b87c6733cd2521a6b818eccc4b412144e449c994d205f2252479e7d2252dc40475d835769201dd0282ab32a99a16570deca4115f909f2125646ec1102514e1008d065eaf1f2ea018e2a5f69f5d5432e042e444463d607df0d355a8b36f54efbaee7e9d427af1beb6c367adfcd585a4721402b41dfd43f4574aa8215237717acd253222e8b47d7344903f820a65f1cdcba1698aa92b32d61bb268c046506cf8c4994b3ed4185b1d5994acc190eed46d712b81b81fd15570d91212e70bd015cfc526d0e1712deefba957f53ae0f5b30b288a5ceb8a5ac1e5df911969ef5723f38f0da19f399f48f38e8fd82c9104f1da5cf0374ca198cc8e2580d55521ef8acaf0bef8586f8e56f934ac873835fd2022ddc6b463bde0eb537cb9a6112e19a150e5aa047aa55bcee5573ee4c719f71bd28202094d153f27ff465c67c85b822dc0a2ba8032b385f708ed2e1ae15232cdf8204cff8bb098bbe22d729cd7a556cb14134ee04897604a440da52acce8ecfad40f9efc6cb35e0856fc006e153db16b6f934e315f5323b81d0b9b5f76435dd1c715156845345bd13d783b46076fa369e502f2558c857d8e0e355db7e3ddb23db5345d09dc9fc649a5fe3df80a81fe604da168c3e90d13579d657e73571039afe63a602b9ae1ef61fc39963ef5cb61b7ba72b09ce5390fa1a2cbb5a892771d54cf79eb09cf25fb4ca1cabfc50bf2a7d0419972b1d377202a16501168d6fafc580109a5266b4c9a29a74c02a0adf54930016abf81d52651fbcec3937dab3d4c2e79dbc44b43c802c21d3a620e8d24ba3c675955647088b4df64270fbe9e4f92099f38bef37bb8175701b10e047744daa5657257d5045c0e9a11e31547bc06d4600da72494d6fac5d7c779cf32c0474884c17075361de0d73bbe235fc85108c5bea52d503997c3c12dac535f95e75d00bd120a6ff9cdbb38a487c8154ba9fa229d2d392565b28cba20d08a09c3468e4281b6a38872fbb230279df90966bc0a0f794466b306d3bb130332663ff51a289b26a032d0dc89ef1b3b4c0c81b9b04bc475045af7971b416c8a9daaf65ad84ceb3778275eb7435e6880a8d2f3d3221f9ae5b74f1b522fce16f1a343295769041516a490a5e2073e222b226267a9ad3cc7562b0505ca429347c9f05d05d2533dfa626dc3cf57245e5f64109c4fc2cc6c6d856609c454e034422505108e21bcd4cb410403bae47b8113ed92ae74652eb79950fc9c8d1c6f3151c70ebc0453e3a45b331c274c15276b64a82d9157682322c6b2af3aec75fe326bdeed0cf986aff789e00a147b84e60d4da9ba4163ba1b7bb6d3c32f570648c925c0ba609252ab4b6de91068abdcbab0e962c1754806797b04689ada13feeaa537c4a0b5c67c204915e42f4cd5a05e3bb5e88bf70c9f8a8d093da859169a019b209c5d538b423e5f3b451d39fbd9c38533a9dce4a96d66ded7dd322f31345721634a24855b8825812086131710c018cc5ea2c86ea439c5ad7e7700a679b3afcf5e9fd1bad7202e2042958254c6e0fd48f0cb0788ed9c2abd8299f6d8bd7746a36058daac6c578b44025a309b70f4872e0d781df7f24544f6d50e71c9cf3dc0ee96c9428d2ec38fffb4ef6ad968685d01624af54220cc4c70f4ca3ec8ce052849de1c715d772085d8e22b8a12941f46250eedaa0c063f6c40a8c803488a255fb59f7a99b1664429340654dfb073c21529f71be14bc42f573aa747f6574a05df88e44ebc61a4cb3599f234660bdfd6c95343ec3340f234306be47033df4a18fb433abcfcbde0ddd890b0cd15d87806a65d9d05ec17203288d08e1a0d87a93f8a09a528933e9bfd2ebed8c4a8df62d20bd128b0cb00f4d5da3520240ce9aef708ae2edd721d763085b65be16652d7c9edad73b700b25ea02d4ab0e18d4637d3a2d22619a12e92717ea8e3f276f8d4ebc03035cdd1a1ef30147be773eda13ee45df912756889b8ab8f567992a55d1af10278a0eb2b623b7b9f345e527f37a4582e24affcab2df8774ede45be50790d1e15b49d21bb07608d17eb315f16ca5069c4ec13b326201c03a4809bea24405f791982c318c9ecf26a26c2c54bac9d6d71546da25533f16cbe076e42c7c7f45bfa136c2363256f87268e8677f986ffbccbe86407ee40bad228ea65dcc1f795a7ef514c89042a50a25f9fe3f7db55f19827dbbfba3b79dbd5f47021c53d89c31581e764f0cda76d70c293c07d30106bbf19c00d3a366b5a109cf7fd62a575fd5dcc4fb73030e2052135f401668cc9bfebbbc61dcaaec111cc1ffe27e8a4c857be15fc72ce00cf65b758b5954caa8a795556111b9ef84d21711901ed722c649bf26ff9ccbf74f5c20a75741200d639c1e8bc79f802ca97e076324186f710b8bfcd569b936bb968a7e3f8b319216f839df4b01c94aeb580f3b7b53b65dc6396c67b6e8175917857b97856fc56ecfc3137f078a7e42a62b6d5769855b6d1ee9c534689fbd7709cd818d9135538c2f2fb5218f275eb7351c1d532d46fcae12b18d9f2fdd5bc939c08e9d64f7a6412d50598cf2da61d7ef14d98bec2edbf1b9535f63741d028d7c73bf3429eee78f120d0edefebb95bedb703cda4cce397d487e756950acf0ad92ff869b04b6f127c6583e4298c9889d2f815da2403f0801b2b979f30e22adb8b60dff13711f96367cd5ac712dcd01f28e08eca80f42ac1bd7819a2d0fc4ea02b832c638d3da2471b642d25518c04c0524f8bef229a4d980f4d21451e962509f627f98d12e1965c86f027a09772679002c58b2fe20466cd9feba13de642b9540fa8bb9137688bf591ae0400fba5a54797dd33253a34b9eae80d6a071fc1e688cc64d88500f02c51eebc253544aec566833d433733d9f19a233eac63468cb1d7ab25bc9e1c20cb985660b42c46ebbc4c0cd256728720cc40815cd5ba50048de43a00528d3b07ce8e1a1aec55d6db84b7d6f40fe18a0d701dc06ea8d25a6152e0729cd9a0422439fb27818b2ed7ec59bf93435d351963d890b92ce6debd9a1f017605ec2d37b82510caabc7e6938fd59e91f98d6f0d9a46579eb303867a757ea98f63249e80bead9e410aae04c6c09435b278876a41e0d9470e215baa2147b3a34fb1fa9a227e69f515d33a7d8b34885cd5eb385ae43bc2d9c55bb29228aad245de72a3be82fdaaab1882ad4e9bfaf971646a52ea8454777d42e2c8aefd7648aa05ee2758d21791bf0055eeab6348bc2e005ca6aa2779d43b915fc3933c000218a30f7b8267383911b3a921aa1cbd43ae2c2ba0941727dde45f80c763bf6ed8d352900065e9622cb295d48271bf65578fd8932268551774bf7b4ded08c751cc97c0ca07a5192bb541df7211781eaec0a8e5b8b36d62f63b820259a2fb483af58b31d73b5c414c165b5f1155bbd763bb0c790562a7d2f3d76bf79739c3c61d6ea287647b5fe1334256f1d5f905812449cf7e622e72f615a99d6504d67045ca12e6bf60bbf88b83feb507cd037c120c5f0ff4f5726bc8190735ad0075a4a9e71735d184cf0ff3fa341164fc0a5e3971fa143b6eb7f56307653e125cafbb07795ee0f1c9fd7da067cf6f27c3fdb0046abeb1dd4770dd6cc70ee78f5187386841040ee13c339aff3b039d0bb6b3f481e28a5bec8a2c624468e71c992623f16dff88a5fe7e54ea1f27075057ac5f94cff8f405f1cf0d137ee4c0c4b1fa6c770c9e443f14d633869415e1007138316e564b7f4db70b6af988e4f58a62e4ccb5756d77a50d9082546b4f6487fa8781f8c49e78380c7a7fb760cbf2c8e525e7fbb3d3f1669fb2402b51a1507a9457ae0469fd8a39c09823b02695eeab845375a912831f358f50bf378e294779cd695acfdecfcb57121e90618d87c5c399e088bdeeec1f7520b9e08c752f031b7c0571a3a31b9427e4f111c0d69c1ddf28de7ffb823bda0d3e60a817b93e413a82f9a70a200d089f6e712827d125b283bcbedaf276e580b5e6a3b49fab5a3845aa118d404d02d83c83b748836b0eb15a522652f02b5c4e7b545cb907b592f4e2a9f7ad3b6a18b7c62457cf3f6ea991d8aa18d7090d9aa961daec106aa3ce0df956794eeb3ec47a50b8c2096a5df131979733a738770ae822bc366e024bbf5617843317ede3f33b7b7a9f1597eeeea3148978a1121361f4ed71e814a15f550ce6961d0230a2f1ab9b7cceabdcf202145b42079ec8e7009cfd6bd655a04d8eef451ea0617d24887992661dae5c72be46d03ff1413251d6bfb3ab7075670a4fca4d0eb6c5c70f758a122689952c3a472b9e734bf231658260e54e271f1d996e46fa4633661233872089a1264976f87d99a1783b6c416f72057fe07643046bc9be3641283a284046a7ca5fdd8985b0808f4b36d60bfc405372ef453beb54a5ba76dbe388de44177298552922181a03b9e386c05a50dc354b641e2f0f4b04cccfe468efb7fa996fec7038b3f46faf6881579d5bcfd1314597fdc4101691c7fc37bd86a114d4e3f9a8a788fdd3583ef811844f3d2cbe218432f72e2099f6eca8d37d25ace94e7587cd02938a6d19e58c85aa471bef45a74021f18b68fbbf7709722d621c1630d7971e6a2eb691e5fbf56637c5bc4b500fcfa506312073e1c715a2528cc56914a05faa403bb51f92541639fbf23ee41d59d1a654318d54b39d5c673f40de741f5ba2059e0fc269d1d65bef02e69c6933a5e1f85b3d1e4828231c3e344418b06fe870e34a7610d0d1b97ff110f00f98a8fa0945b9dd7dbe117faf41cc7f3f7939bb800e821cb0a387d578b5cfe0f0a6d5a2a8237f6b21eda8c7fa514ff85181f739491d9c895e73b18564b3bbf50bc9f2a78716a99d65aaf8843583566a299c83f78e7f4486cda378343de9c4cb473d485e052f8c9363a67d81a7c0ac6e0f55cc25454e06ec7a3e5c8d1fc34b98878ba0bccbd43955ad785e3dbe9f73bbfe63b1c66812b049e2e085f8db86858e7bf50d519744c7b6c0cbbcbdcfef806bf6f4cbd38b9951c85cd569094ed98a4ec5ffe5adc17d3a40219c7dab2f2e6960422f8ef21df192b091f95e938ef8ccb7ae958f848cd7fa951c2e820e47420749c35405be088bdc41f296488d91671bdeed96af874e71795c394e930361bd2af1c8fb27110b2802af11410545d0d3c8f497abe38a497c595e7b77664ed5461f0bd9f22f59127eaa30876a05d60f9a1c064fbc1040e33eccb411c491ce7ecbcf6bd2159f6cab7ebd85b4716d4c1135e12cf6b83645651d7c6edd35b07ff1562a76b8b5cb43c153c4dcf734e9b0fa7c35597e03e95068e74afdfee30b3fea917de283add8b943cacbcee72ed73b2dfec664f246d8f6f2c285b47efcc907c37b404db59ca05aa55fb5dadf1c295a1c7324b83fa1ef9959fa90b61464508170e22b7dece44a16c573c6fee4110d5765bc7a5029e801ce2cf89cf9d024c1776bf729da8309e55ab837cf0b46d844eb12d5eba2fefddac2cb6cf4cac0204b66df26662dea648cf265e2f9fcda2a4d93c3916377aad6c1c05a877c52dc73f7ef2162c321be09034f6c1dcb231fc36811bf0b132f63d8ad2329f69457bf351590e3f7e70f0b67def5fefe29de0d0c020bd919ffe9bf73bc27cbe0d8ea683fcce702dc2bd6eb8b1281a4f0884725788d824a6170a2ea8919f4b1339ef1ae728dc08a3e9486ea35b5aa8a6c236d131c8af40872c47dc44ea7c2fb5d8d41610ee4574a7ba902ca20c5ed36a71365ec6fc41897b07e28efc4905a0d97f45cbf2452e702bd9fd18130fdefb958d491dbc1e77f1fb329740ff9f5cf565b4e9053d363f92745ccbb24dfdccef4ecd99fd43505192c5ed8c8bebca56f6ada309e00cbf1e878c7192f10dfef56afe0131134ff3442c34d98f8fee1afd741706d85389f7c4aec199bf3fc45e16c57b7d05296b7d754af2f11441c62d70d4526394629f9bec184e169ff9613dd08cf55e58e94ee3b3e388c1e283fe60da73313dd1e3efae77c4277feabc7f873991c49c798520dc2affb92a772253fbe8341fdf59e293bacc33e1fc3bdae8d8ce2069bc2a3e76000779ce34123d1ef57470bb6af763e308b23b9b031bf2ff5757bf5c88c04b9c90c1691d019e2f65e0007a97b112b6160df0c0443a017f701c2f2442a8fbf549a36fe9ed7861d7d1cfddcf7dc5f1a8e458a02aa8dd1091ae4a1008516e9482a39830b351107aea4c8adb200a1bb85c5dbce28a0074c419ea39b03e13300d283d084aefdd14bf1779b2478a74099bd46ac63171ed015be45aad85e1d1e5b1f71ecd1ee5a3232bf0b42d48a6beef76a28d5f28bca7538165007385508a1bbfba96c9ce0e58b1f12da226b23e7085a09422998f4f77b23d066204c52a96fc12589d226d872851a9c9c4c9e9e84a3da2bbdd8a3a6e58a78ccf287835f03a178a17833bd0de3f820e2428e01885774a91f4454a34e80c13e73921c7ca83c58055229ce27a60ec4207a8465becc113a3709a1ae5dc12ecdcace204c7d735c78ce0bcb7ca9c32637ff9d68fc5aebfe4a9d59d0713670460ea031238a2105c20f20796db5448f4a722d89643c0b5e2803cc1139c72007fc7c89cd824328f96bf04e7eec59f75f6cfde86346db450d2a60f448db03cfc94949e4054e21bcc9fe6da14b2c2db21f4952b4c85fcff3b72e0627898b009ae1d15cf768def56addf6a183aeee09e441986aa859a65e29f0f9a14d0deff2dbbb6cfbe69a6bd6232cf8607b4ba81c83ac1cbaa42830103a788b27bcfdae4c0702ac7204faf8eb012959c693d9e9143c165222a333f185f3aad049b80bdc2e4eccfd8eec2f8f01c039050c394a2095d13893f6a645df7dcc6a7c3954b47432dbea79cc50c858df1e32d62e3f4d5d26eac6f57b39353d3639c5a801fa7036787860548d806eef495cc48e00cccda6a4ce16b35d82da415b0ce5a0b8a40aeeade3ad52e159ec9d5546069fbb0ed05454fb4d6f83d551726551206d55eab5dd024081d426ed010a221fd9d1138448d70e8cfd0e009caa1cb3f47a220cc09e1ac1c227697a7a43aca862ea334f411bfc8e1766e024aa1e04c3b03f9242a0856b2de7c7db49dc235d820a774e69ecdfd2d2bf0f809007c1160c549ac93d39ce43c8fd64bd23c1eb20733bc4db1d40b5088cdd6eedadf3131c7c06f6f8b6090f4fdc0266970a7506316b54cca4c17ca320cd6b9081af3ce635ab1f69f0a51abe712e01254ee4535f3cf78de3252bf689ace9fd6f3b18c329468352506a23b1105213696a57d9bf1db1a466b8be33b09db1758d7d2b67334052509bd665be579ae76044bf5e4e7873d8de811cc19820bba3beb120722b3ce70a0d579335709dc9fffcdc92ea77af46d8bd62bf9bcbb2d0fcd4435040f644c15fc89ee729fa983fc825351cce39804b47e79d6b663015ce7dfec4b7b85a7f0b654e20f4f45856470707bed73e6a6072926d4d050f079550e3e25d8785c8c7f8bec638d8141262cacea4e5f36072ba892effc373ee8109cc211438a156a463ff7269cb42417e7207b5f6014179cdc60a6843054afc3a9dfbd188c4c86f02d8f7fa2098a8649dda2dc4e9a87c12a22202cfc925c38c87739d58ae607c180c24fdd64767e45ad0a465f8252134dcd17b49425d690634e3acfeaa004175c5504206dad080e2c5f396504a123bfb66caf21564a12cec8c577d835796b2f4dfbe701a9a10421d729a4bdeff2e9f777378a72bd393fe3344e27a1a97d4c10ade942ad4d0d75e1784c3798ac48a6376834989f719d03d5bac42cefc307c73d2cb7334a9115e14944d446fbe05abe51f2f9479af666bfa66e96ca87e33be7985084c24c498e406f19edfe9fcd79d7e8047ee180a9385ab2ba68e87d7f9256a97dfe4cf051bc1277de63e30ec205c4171907d6d416621be53e52e0a333609b5173451c036ac74ac50f5018e0350874af13c094c0706b15d09de8130d8c25d3e0b1b364edb137bae3ee4f719ac46dac4868770f8c198578ecf73180d3f38ae787cdbe9425df16259f681020ce783e349fe6d3576e2b9361223e43db7e48c95ca252636b221b9d4040d0fa5eea91b5658e92b6a5876e75eebef55aac3261762360124d42fd8884596908b87567c274c060b75deb1a15960d0c948a42a74287d55311199ed180b172a1a0c855aa2e43541516879fdc3bbb0bb7ecd637fb57c5dd877636025ef8bc38722f97d493e504824f882aee33a4507b13a6978f4e4b61aa2a203f56a8c1a69284f9a820df4a746bdc23663f39985c3dce18e1b7f6fd95d1b7e5ea31bc2460a12287cecdb589eecaee0d9eb2752cb1ae54024bc54f65c049fb834fe3d48f47a298e9892905c6aa0bef867ce50683586123502f9a7e5e45b854a297881481869bc035742f8432854bfdf78036ad0d76e9b85d88c93730c0d746c5e4deda03192c1d045ef01f017d9cdf696b1c651022a221a73053c2bb2c3796372c12b97e5ee62a53097046f027eebee95a836b5c1c8c1e81a47f84af8103a55c0a3d5c95430dd8e8725599e7ef1ed37f8bb69d8b494592ef49b8afbb31581e5ed7ca3faa430f272d812b91d4319752894ad7fdeeaf64defbab0e3dd18daa8ce5aab0d08087f1a885b34c1611ad80e7af24d124c04e0bde5d259ee925ce6120c7095908dd34ea2859bc4100a761042734c56289edc7e522a3389a2f6331aa337a05883110ab0e73f31f4e98d67055b2d8e0fd49adaf89f3897045f8adfc2f5a9f2223ce1f45e1b4dd225150ad032be70e0d12930e1e72061721926a85b47b8fe787700d2bf77a3b034b84c07fa7a2b4c289c6bcb8110be582f6e02f7866ab44e072beec22e34100affe6315f7a39eb4aa9801c5c63136dde859a3820707f1271e24ba5118da62ca07638141bd5485304a4d042eef450a82fa4a49c37affe4c0f4afc0d5e36fb420c2e3e56d6c05371bd64e91111d794599e6eceef29375b79e32a5c040b716b5f0e9901893d17d363f9da18904001356b0a6e9d261f42bc50caef734411589d49323f70b91c3f8bcc8ae8011a4009466e1392cb760822721c39c6c2dff7a161a0b97ac7633efda4f154f6040a0fb78912f553793171e2b4733f7403b4cfc8969b33336fdb4770101a108a94c0470df25d6f5c2198742fc68e976b58ad775bfeb2a8ce0d065c6254c330913d0e17a7b007f84d4347f3545343d4fd2ecb623c81f64a058915967b1d89dc0a1867edd077ed0ab9a06562e4859c2e5ca2514fb239d8493619ba71eef7e2d0246cd447323c84a669aec33cc4edd3061ec7ed040107c3431f0c8cc07e59526b0267c1559c79f6e36d48dafef4a4d6444e91cbb4d8aa46bca55d837cca41b9bb35f7957050edbe225abb5696d4373ffe696255490d1dc17182c685ff8678d7382a85ffa7f10ecbe3a9ed084dbe8d45bedee6b4f910cf07b999ac1a359a5dfcccb41a1cde495b4258b370f52b01bae6788a07e7accd5b99dc8a0b657f794a8231640cadf72c20e65af9f0bc20088a24499aa625aeeb36cde77f9ba3c4ef35806364abbb387d5b9a258a896e2a38bd39bffc0d30f46f9b5eb1420c2cf756e748248296bcf4681e229cf5c9b86acd53351ec3d54f37454fd96718052f62368db591003294d1b8c47bb91150a8e40e77590aa823a53f84758a7fb899e6797e454345790d2f8e717099039158836eca11e59e3a1284e03302ff7530f10785ef5e1f6d6ea78377e3bbd16ea9ae7c6cc5503c4b34bfeb44519d209a48d155a5104a51de5da05594672a58d2d8d7a8459bb4fbf5e5e5b1e647e371146d7e0cb4f06242ea94ca63cf9870dd762f4848eda03522e90fea4059a666f0dd3b8f76956acda6a80439c93fa37564f0254428ba3516cdd3ecdbd6fd2fed6c8d9db03dbcf5c3bde0ae9a2acce0222bb279f7f6951e3c154f0a5e8e77058df199455152abb7e10a2939bb2a2218cbd1638ab3dd83fe2e9977ab2d94e842c72a5f08d47f4c44363f249e0caeb9b3274a7ffb98494123bc0264c58ee3377fc5998668f365cd05703f0c45c1ecbf1fc266bac186eaf85aa6b8985e4b0b25c8943f081724a533e3a0121ebfb25058b120cda70b922e44dc91c414bc3731853a82ea0abbb94f326e6718b8aecf67f6cbb9c65b06ea9167efcbe8582cc479bfc360cbb8290cdeba3acc26b3e9739065bf43f363d8bc5df3b64bfd5511f333dc978e423fa6519426d0bab46dfd63ef1979619f1fcb597b2934c36cc25b4dab85de89cddb204a2ee8f8b2d91fe2668cbdbcfdf1b5d248eac1d7e79bc35ceb906766faebc19abcab2aa0ef4f78b87119757bea6c04ed4bdcb73dee3bfeb1310f43c1478d33115d41a4d619e3d3d4ba8f68a77619c0e4fa85dd040fff7b46eb52d120d60312765874f5479f0d90ebd6de0fa9ded1ba307d120075cd483cde4e0bde94acfc8f2bff890a691a31c8c32f116efc3e669ba2589c6bcdc2fe099cb25e4fe125328e7fd66197884a8a449fd42f6727ac174fddb0523a8299e435a33a90a3f94223577ec697fad528ec5e93dc78e242f410aa3c849c2f350e19b8d1b676d7eea69a39cc929d5be0cae8f496043fa5bbe639b34f8dfbdc95632acce18af3cfb78c561ca2b00b53f7458539743e0d513c4483f3543b8c0f2d9cd07555489190454cf5de8707d886f6562fe7360ab81248ae4ec6b0f39374f26fbdd173e163272e506099c2e55b20379bdc2d4295f57b319a5713be95e9a992e795960d917111e5ec298bcc16f2399b098a973cf12c7799ecf77ee4e0525e04d8f8a3af150c17dc7644de7743f99f64c40c524c5a105b9d1b3e22e477865639a83479a8f2daa532afc2b68da152ae378ec9683dd9ef06167223b002fb697d849b3f531537d6f3623ea7cc5ce3f499a5dacbb0772f635e1f2cf879f1dfefbddab6eb44f08048b6f40623249083534531bd797b3ffc6710c813765819eb53364f70a0d815693e570479327f8bf4b6f48b22dac58ebfd8e60382fe9d0143429a03646dd8c66176f674e096353494f45ebbebbc44f22919cad023db5314b61391cc333eb0137e4526e3737238c2a788853eb84481488313f2b981369f2ffc4e984e3cf7cb0e4abbed5e9a2c1dd7c17203fbb3dc7d80df7cbcb9b2e690515111366e04e70307f1337215f7ae3f3977bb30a105d3e8b0f50d303a6a133de15b7c40cdd7c517120927ad4d5ef3a42f37da89450048f9da3d3d65453bb368c8555c23f983d741de8f56f0bb11b6e2a526861afe9e71c8a3ea956d4fd5d6963fd6004d8971af107afc452eeca1cc798d34e37ce21a5fca57f44b088bc5956d8940280ab845fef9600c47a8e4e07f2516eafe7933a657848606499d75faa38c5714ca978a79db79a0af4b2221ab3dc438fb206533785c80385be51000ae7c04c5aa5389ff62f0956fcad57a2199277c26d27d56fd14691bbcce15f126804f5d53bd73bd2e0afc589f4e20c947c965d367a0246893cb1af06a9fbcae32745bb274558d689ccf13e8ad2189f8ce2337ea25e941d7294f07af40e019fe9a1157e93b336285241ce18230f6bf1fd6853e8786cb0928749afed782d2e4eb70614bf98f7dad425db94e073164c1cd21ee9e8d2a3873b2536ea2e5a60e23c214698ff64da834822df247638843ccd0777c4917057768c7f4d70da8956c65eb7d22db64590d3e3c514b7fb14b467ed009b58c6547c469124d2ccddc55bfdca55096183b506ebf62c163bec2981d271027e57b904e22e85048ffd5e06d903209f30993dccfdba10ed7db663d2d53105646f61daff0fe41a7bfd27076e55b49999129a0ea7e6e8d7b438ce7e678e86ba691348d70084d126ea840dee71358d7acb00906314e008cf58bc818a4e87a33c7dcabfb41036d1ce8279762aba8e577251452fef824493e08ae48c5ec47835691d8f66468e64d72f6cf806a90c205d60d14d84eed86df25be0967555f908b751c68afab6654883428e8720973e2423a74b4db78bb13483f6af8c5cc583f78430bcaeceef07735d6a75d41474ef8586b04846380ec9d5a758441d37ea1ba59f58b23c314b948f7f75966bf4db44520417a294bed69a2fcfe59e4c78581cb8f9fcfc6b4897a51dbc6f72eaf91a4c25aee8220636d8c5889ea233fce949149c763c80b2479547b7e8dc1724f67b80241356c6741bedf7da9812f84be4d0c07a28475f141e8307dc3afccaa854dbe1aa3f23f0c61bc311dd53d42caa7ba5694845f27e9faae129c8b70342401f182c34ef905a2833931753d8c3ee55324af43d33bf9272f14c89fda89247d6ce9f4186146de240206699ed59a5ff32a6a3611084b3a3483e52587d4e7316fdb4f07076e66fd8b9e780946613d5fb97bc308808932940cc5ec79d12cfe6cb29546b76654772cf00958958dc028f368dea276cddefc97e3b82516972553d43ac34a78aefc87f4509a20ff12525713a96fc1c28dbfea250bda5b7a97a29f7ef5119a1f7930f9a811c9281af9f8cb6b327640c14361727f8789f2e0eb327e870ff3a312a50652a945cf7a6dfad0c8dcf6223a87946835f45dbe14f84dfbfef9c68b9add1594f086aeed3ed7b19f8d82997d0b4e4ded0ce56d2fd083901f2cb0ab7599e26bfdc832880a5cb568eac7b1ef702f03cdb93e6585a58dc5cd4601f5a6f57aa51132281979f73b705af8f5e2d2482e841e8f1e8545a0eb257ffdfd6785c07b3e76267acd97379071c50d132e1c64caac51a644bb7f32f58427f8cc885268c5c05d47a1ae4a94920aea57f09ef84ed286e28aded8b68bdc7d35505f69eef2571a2f1a4ef908cffd403f988c42a254d1b8be4b51b8e305edd2278976a7df018df0acfe39116fd6210ca048250da715789ba29b83e62d1d3bb1231b7d408999c405ee100c837cc0be75c309c5aa1b9362a8ddf357b9ec260fb3f2cf07f4fff7c125f689db882296d83dafc166f3eb49db346192dd77b475bbaba4fc83048dec0f888037ee56a14dff3bd851423f4e53983a25f950921c5c38b4a48c3655e8e4446a0fc15ba3e98bc1725e0bb80b7e2c6d28b42589b566033ac3f2a6365593ee0de77fae90080d456f08eca42dc5572478846d1d0a6f20cdf4ede530502e9e16c9c645894159f81159df66d2d6f7b3180f3092e20d3ba81d2a756f182fdb0e2b655ae73abe404ad1fd14ca8d0745f39307281ea8607e3d1c46dd6eef9716b46d43a577d42159a4be7856f1df501b854a59507e1abee846457ad444da6da3af614bd3467303580e0a10e80681c1750d05b4ceb0def541099e150de26e816fd89d8aa84b3f51d15ea7b5b42424bdb49156675699377432b188864ce233a1a67f8d7f90fc448890a013af65f676b785dd5f2f61c7a9a678c3015e66bd5610ff660e7ad8190efd46952eb9a65e65571816abdef3276bf40fe78aa4f975b3c6e389417b35f0982a1e1f97604d2bfd1b5bf0e54281a2061d2ba482b7543782078f373f29ed3857275ecf2657d0f03218727cb0086e6f34fa205482fed2e046a9e644a529444cebb9b4c987922ff0595351786248a623d0feeb8d5fca6b77a1ac69cf2be571e146b0af210955448a97f385ed1062303ada66eba070581d388757abb31093f8d5b333e843438a48f11b3334436b9a911661b80bc2f7f7ba3f48c1c09f2b6846014b1e6fb518960e49b6c3641dbfa512bbf731ce339ff8c0e9301f4e825633c8fbbb26707c6516753a9464307a6dd861236bf01b603c5910bc297cb4a4914ca451c5de2a640b7e097357583447b366e2be5ad8fdfbae1c30847c21e405976f601f5dbcc8c5d2882797fca4cf3c466f776a383ee198722a18bb11a7a10b04c00b0577265e0153d61d9b5627f914969a96917f9485aaf48977a335047c91c226303b8f6f4cf42d6d0f4687c11b7a226ee357a0edb281da4b925141d5fa57d13dbfb07b25427b43df4d8b1c77887c68a4094ab67aca3409d7c69bb28294eb189134716294f7558a82e1c3f57712bf7b3b1b4f4ba1aa69c65e7744911562c39f76f0880576948f409e0389e34c45ec5871702b82e138f72a3ad843cdd070b1b517645ae06b823ebaabb92bf1f6101dd09c04c1de5ed1c0a4aafebb31cd4a7ef3826dae10eabf55bf03175a5061c95fc5fc3f56cee3057e1fdbf2fbfe2b7a3b98c1a1ecb23d4d2f24e76c9753b9768e728ee55430fffbf0fdf534bc65f37817b411e820740fe2b338dc4c929bf9a8c521d066f011cfc2edc25394601abcd10a642c5b7fed191e4ba942dd0d652267c1b622d9489072338fa0a83b4fb434516b6e35157ad3b3877ff5a843061242a05b02e57c67ab8ccb63362bb02da1022efd89f6188ff13aa6365bdb58da26c69518d7739b4600a91da8938756e12f0faa7267a3d97395c447704683d623a9fdf3b93df1bd9dd0db04720a8c9a213be08cbd92bea7789266e9a3f4860dc4d57c6dd0bcc16583220493a8859ef8860e04a7f5d11aa5fb4df1d7e514d9973b618369e37a4b31962a3e5ae1482de07d1e96611db3d59bc88c912a9a269e8067dc86b75904a08110640605162039704621f15492c092ddd37d078386cd910694808485ea9c76ad285e72cab7c0cff2052e2068ef87f46898eb8b5b79411be288c42b072b121c21a829db034c10a6cc0358b85ad1345b510ffcb57b5980cfcc002ac9110b450bbf407d9e3a4b98864843d59938909d6a4dbb3a78634f2002991302407aa01b1931d08f76eefcaed73dd0f0c353a78ecf8fadeec2b9bddcd6ae6106e159b372e1e999fe39d75ed7c40357cc8dba48473cda167a20427a077a0d36e42b81ba5e7ab8f0a0cb5c27dfdb24f82600c525a63f3b9f141aaf992c6b0d709caa984f24f77a8094a8f3d587a9539d42c1f07ff557f205d0027dd10b6397a4209f6fe9edd02ea6f643472c2ab7bb182cda3da828ed972ba56f0c119b2d5896845f4f7b70d7cdf93b0d084d0c98d4b0c49192b59c003d10b50e291d6810f403201a97ccdd50a8a7476b90619fdac07ba5483b4e6600239ea2a22059411ea0e1e0565f20d9d6e4cfc64b152dbb7907b973022f605264a22c917fb7eeeca83aa13d76cc31c9c8d712e8098cd062d560179e05f5e13dbedf5bb4214870255935c90e20a6d4c2c009a54af2e5e39bb93f377efeaa8eccf3c2429141309c6820c5f3ac4c7e721471fb6933eebb38d2ca41b1df24ac1da8350fa05213fcccaacaeed3d34cc54878f6fabba0d349d06d00f1c96e043a84815ea65f37abdad442048e034312b139e801d074ccbe7ad810029e776f457bc44b0ce83f4c1076110086f5c8a04dde38bbe9e14830fa00ec2cb3914689118b87e9e1da4921d0ef028fffc5b3be1226a91637fe8b35141b9f50c5938fba213476d3e3cd5163c9137939cb5e86d566428640cc2759a911cdc5a013bb0e4fb208d17d6cb07624fdf6147c3f6102c9ae19b2a0533a88bd53f99c5c935a6389f75e3c1092e26b0031ebf983ac1cbe236277f5ab7b4898557e5e5ca4e9d543a045c57bc9196b9d10bf3be8e8677aa7afc693ddc2669f5400065082c38368b6f9ba77258bcf4f0ed72d4f10070197e5ebaaf1d225ba828383a35204c652aa0b2a6912c57bebeeb400a307475a66f20584b0e3bdd76b1eaefc7838d89ada735727b0be5ca05334b86519adc0290a1dbf2d45e0acb3e266649ef9bb278bf7c860d8efdd944cc2146e5009c6924ca430a6bfd1e7469d76d441954c42202620283a8b4606aa2e11bc71728d10de6096e8fac164d080124c8178521bc87f64dfd11301b5717683d2df44da2fcdb3e22e649e71c94ae32656e0eaadde83dec70da5b04821d6965f27f7ced3faf30640d6acd3b2bef3f964f1dd98c477492e8362b5bbea0aa3d081217f228130a73f9cb85deec0e73e015cc209a22e8749160d4a97a853ddd78182dae4524f2868cf766a13593a4867df9e5c34bb01e72061bb612ed9bf53e77a20e58ef2a9445903ef9f82472680f2abe33c26d7be299352ca2381a62ff8f34e37b47fa36caf79042c50d6afc44d765a18416ee397146548b9d71087f83583437884cb48db0e40b084b171fd76632a8e96d9c4f1486ab4954110f56b3f6f9be80bc6963c25511342e88ad166e92a8a553e62354a2f183e18947b133715973ac1b288a64575d7012b446614e0d4d46b52240497d047cf23aad4f7175ac728191f3266ed5abd8348172798495e01f3cc019da7e7ba03d2e1ced2f479da5ec5e59eee19bb3a352f0cafde40b02cf3f9e13094a2ac691c5a4f3b41c3c2d136e35f551e476e263eb3b292dbf49cf40650d0991394c7dbd5531092546c288770fef19aefe9defa4a2ecf098d5c01c226fe6ab08bb9be863de1e2db3b245d8a005e9f7e3907d1928301113eadb80937c7f5450707b9b397899b8b312135846d4cada7920f979deb913d934731e4bd80dfddc3f815547e630c3f5eacc0a28fc479b968a5ed5b2f279e2ddd11c731b4e129f080d91ae022b71bb9eb905a09e4e7df4ea520f40adc00bf6752e6c0d581288a8e12b23ccb93e20431351d301beb817cc292d6b678fd6ded4f9bde8b9bebe8744d3fe7dbb376c024f4fd0bdc2982579cf80ccf9f8a7989c2864d1cade53eb2be653da833bf1394908bad50a1eac553afc4d5819709d1a3df4592bc8e953bbeea7645585cb55e50eb2eb0e98c4a8094a1dfd2a61f0ec924fd21c81ff7b3ae3c7f2191968bfdf551c392f44f71af9c0fae65edf643a13a3ab48eb57df31edd932c8bdbc10ea75302a94b4cee1d8aaeece5f022b2455cea1085a7308fd9c7a357450523987442e0c4502b156a940d18815148bb85e7c1f12b460f2902cc2595f94c794680f53c7a7bf48998d4cbbee45a20420a24fd08c422863538d878068a84e06399d49772f8a003c397d480beda1fc9149e1cdf669a278c46866342e2ee6f04c7537fa2eea87c0f89a067f573cfc95729dfdd6f1c314d1b8854cc8abfe6228f89a4e4e24d1c71a574220c767ef62ffaf4dc41504e458b3dc1a11a0f4845f4200a21a2f8a901b0529700d7123b7e17412eeea1e834a7e7f84e469d3dc39b8696389b489d692eab7350c2cc279ee8618774f5eac9aa4d8d657b2bb5e9a6a0001b324fe322c101bc8b97a487384fc883b96b3f0fe6743d158ffab72722e8a2d031499ece20f6ccf111fb3ae556b4b1fd074231fe39be217303fc791ce73e5971747594f28b5332ee2d60399f69effb4a9d782223087b1e8694911be31824ceeabf9f2023baefe95e2145f1b0a2133b392bcfd4d87eb0f2c57f81b51df3579cbe92777ceeebc43f2e7ec2c43e61f9b1faeb6ef014022d1f6d16c73235b8be3e0d4f4b2ff3848834e7fc462b167c033efefd54bcf5eb945c61e932cd053411e7abe32ecdee27bef7e55550881536a52c62efa7a42b15230d69f5f0fc71433387ad22888e58b741dacde8690ac6c38b703249dc093005c67ec9cf45f112160446f1921aee5bbd9f020f5d384abb0c9d7b1b3f75ae8bc80792c4dd6b3cc63d5e2ab3e5fbcc646996bef5afe5f6db808c0bd8d2ef933c05c27e68404407cf8554274a8642b0cfbde581ce9a4d4460f9c4bd01bc84845ab2f83d4a84f0948c646eaf345080afa45f319033d995b2b9b1c50efc044f427a9ce0a492bed353f5b8575caf834a184c33b2b2833d3d03085383a79b64a60237ed0981a33d2606599cdab4250eb25b7f14fa4d8d142a4c18225354ab1488a1a780f31744a77314c0654c01015dc58c95a9db31ed71545d39a054a8da9fefaa76faf47308cbd506afff389430143c3796ebf5959f5ad2d6d3cb0c3d239480fbeaf6b38b9620ad992a4b128b773695d864569e17666a7ffb70dc2460f176931a83e4786e2181d5bd652404eee57a907135e45ff372053c5be2b1f8caa12b7bc2bbb5a728aef5b95a12c5d3da85afd3a6dc0253cb0e4b353e9e69b804dfaf03117904b690c13456384f0827ec8dcb79ae346b0faed299c22948847556652f34c372a70b758645f829ec88f0a65e1d44844592224c1ed240e72ba8aa2e3139d8c6109c4548d36452de9cd27473f1f5c0fa34f2f271a45cc51d0e55fe861ff00025310fbf51bb659db512c7172b6605c6555224afaa2e2e2c7bdfd78f6ff310e830ffecc72953feb91f71c54d55389f22de2d7c74b5411c61923a1dc0d01ad3b58009859d56ce9733042a670c9c7b7abb4a1c9fcfd4e5d3028d330a7c9c97d184afa7307e28ef701f0b5daa4c5230b541eb2abee50d2f49ea31b192300321a9cd10b0f59c7f95066fd53cb819c29ae77bbff6d1be10a586cbb3e2c16aefc3b4df0ecbd378951d17276afdd60445beb87a457d3d102e64fe19ed5160eb94debcb1dd77228cdfaf5e1b1ce9e0b971607ebbb7adf16ce244dec6c6f0fc5675226acf83f3b892eed6402fe57664563d758c7099e417fa091916bf05ee89b7917e137429d316f1aa6d7a79b3e99d7d8f176e6be6fc0df1e9973957edca7d85d7b5ca1af1218dcb127f8c65a06632557365b815aa6c87963a6e4f60737bfcdc8497f9bd3110bcca75c5383ddd2162ab8ae776834e24927cfdb897597b537c895175dde1f2e555866666de830bcb7ee3d5fff6d62c265ffcc539f3541ecf9da4c8f649b0f70b84392645b628197354d53ab21aaceb2dce5dd073b5ecf143a239edf18497d6295d2797c295c2c274c11a24d2b8c07c6881131a2e3b7de9fca19f6dfa31a0d84f8fc8eab200efc2ba07b2c1e01148f3649d96dcfb501f0e10b1756360984a0aa0b4c25ac983bbcbf96c2cc5407db59231f1cb6f07edac3cfd650df090add94f4eb186a3e7b2c27a18a4f5e693877df9c1c531776913c23a9a882f9a24f8e429c031109f86459706ffdf2bc3b37f875fc0ed62a414bdfea3f48b5cfdf7cefda75507042dfd7f7d6ee4bf50fe1f958f528f73e0bfe4252cdadc1c85e0bf5600fce93dde76571d7773fed981fa2b64a4b5d31142fffce7fffac7dffefef72ccd3bb44efb58fcefadf995ffe7eff97494eb3ffef67fff5f000000ffff8a71c36e1edc0200") -} diff --git a/wailsruntimeassets/bridge/wailsbridge.js b/wailsruntimeassets/bridge/wailsbridge.js deleted file mode 100644 index 10df3575c..000000000 --- a/wailsruntimeassets/bridge/wailsbridge.js +++ /dev/null @@ -1,215 +0,0 @@ -/* - Wails Bridge (c) 2019-present Lea Anthony - - This library creates a bridge between your application - and the frontend, allowing you to develop your app using - standard tooling (browser extensions, live reload, etc). - - Usage: - ``` - import Bridge from "./wailsbridge"; - Bridge.Start(startApp); - ``` - - The given callback (startApp in the example) will be called - when the bridge has successfully initialised. It passes the - window.wails object back, in case it is not accessible directly. -*/ - -// Bridge object -window.wailsbridge = { - reconnectOverlay: null, - reconnectTimer: 300, - wsURL: "ws://localhost:34115/bridge", - connectionState: null, - config: {}, - websocket: null, - callback: null, - overlayHTML: - '
Wails Bridge


Waiting for backend
', - overlayCSS: - ".wails-reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);font-family:sans-serif;display:none;z-index:999999}.wails-reconnect-overlay-content{padding:20px 30px;text-align:center;width:20em;position:relative;height:14em;border-radius:1em;margin:5% auto 0;background-color:#fff;box-shadow:1px 1px 20px 3px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAMAAACPpbA7AAAAqFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAEBAQAAAAAAAAAAAAEBAQEBAQDAwMBAQEAAAABAQEAAAAAAAAAAAABAQEAAAAAAAACAgICAgIBAQEAAAAAAAAAAAAAAAAAAAAAAAABAQEAAAACAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQWKCj6oAAAAN3RSTlMALiIqDhkGBAswJjP0GxP6NR4W9/ztjRDMhWU50G9g5eHXvbZ9XEI9xZTcqZl2aldKo55QwoCvZUgzhAAAAs9JREFUSMeNleeWqjAUhU0BCaH3Itiw9zKT93+zG02QK1hm/5HF+jzZJ6fQe6cyXE+jg9X7o9wxuylIIf4Tv2V3+bOrEXnf8dwQ/KQIGDN2/S+4OmVCVXL/ScBnfibxURqIByP/hONE8r8T+bDMlQ98KSl7Y8hzjpS8v1qtDh8u5f8KQpGpfnPPhqG8JeogN37Hq9eaN2xRhIwAaGnvws8F1ShxqK5ob2twYi1FAMD4rXsYtnC/JEiRbl4cUrCWhnMCLRFemXezXbb59QK4WASOsm6n2W1+4CBT2JmtzQ6fsrbGubR/NFbd2g5Y179+5w/GEHaKsHjYCet7CgrXU3txarNC7YxOVJtIj4/ERzMdZfzc31hp+8cD6eGILgarZY9uZ12hAs03vfBD9C171gS5Omz7OcvxALQIn4u8RRBBBcsi9WW2woO9ipLgfzpYlggg3ZRdROUC8KT7QLqq3W9KB5BbdFVg4929kdwp6+qaZnMCCNBdj+NyN1W885Ry/AL3D4AQbsVV4noCiM/C83kyYq80XlDAYQtralOiDzoRAHlotWl8q2tjvYlOgcg1A8jEApZa+C06TBdAz2Qv0wu11I/zZOyJQ6EwGez2P2b8PIQr1hwwnAZsAxwA4UAYOyXUxM/xp6tHAn4GUmPGM9R28oVxgC0e/zQJJI6DyhyZ1r7uzRQhpcW7x7vTaWSzKSG6aep77kroTEl3U81uSVaUTtgEINfC8epx+Q4F9SpplHG84Ek6m4RAq9/TLkOBrxyeuddZhHvGIp1XXfFy3Z3vtwNblKGiDn+J+92vwwABHghj7HnzlS1H5kB49AZvdGCFgiBPq69qfXPr3y++yilF0ON4R8eR7spAsLpZ95NqAW5tab1c4vkZm6aleajchMwYTdILQQTwE2OV411ZM9WztDjPql12caBi6gDpUKmDd4U1XNdQxZ4LIXQ5/Tr4P7I9tYcFrDK3AAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:center}.wails-reconnect-overlay-title{font-size:2em}.wails-reconnect-overlay-message{font-size:1.3em}.wails-reconnect-overlay-loadingspinner{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#3E67EC #eee #eee;border-radius:50%;animation:loadingspin 1s linear infinite;margin:auto;padding:2.5em}@keyframes loadingspin{100%{transform:rotate(360deg)}}", - log: function(message) { - // eslint-disable-next-line - console.log( - "%c wails bridge %c " + message + " ", - "background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem", - "background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem" - ); - } -}; - -// Adapted from webview - thanks zserge! -function injectCSS(css) { - var elem = document.createElement("style"); - elem.setAttribute("type", "text/css"); - if (elem.styleSheet) { - elem.styleSheet.cssText = css; - } else { - elem.appendChild(document.createTextNode(css)); - } - var head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(elem); -} - -// Creates a node in the Dom -function createNode(parent, elementType, id, className, content) { - var d = document.createElement(elementType); - if (id) { - d.id = id; - } - if (className) { - d.className = className; - } - if (content) { - d.innerHTML = content; - } - parent.appendChild(d); - return d; -} - -// Sets up the overlay -function setupOverlay() { - var body = document.body; - var wailsBridgeNode = createNode(body, "div", "wails-bridge"); - wailsBridgeNode.innerHTML = window.wailsbridge.overlayHTML; - - // Inject the overlay CSS - injectCSS(window.wailsbridge.overlayCSS); -} - -// Start the Wails Bridge -function startBridge() { - // Setup the overlay - setupOverlay(); - - window.wailsbridge.websocket = null; - window.wailsbridge.connectTimer = null; - window.wailsbridge.reconnectOverlay = document.querySelector( - ".wails-reconnect-overlay" - ); - window.wailsbridge.connectionState = "disconnected"; - - // Shows the overlay - function showReconnectOverlay() { - window.wailsbridge.reconnectOverlay.style.display = "block"; - } - - // Hides the overlay - function hideReconnectOverlay() { - window.wailsbridge.reconnectOverlay.style.display = "none"; - } - - // Bridge external.invoke - window.external = { - invoke: function(msg) { - window.wailsbridge.websocket.send(msg); - } - }; - - // Adds a script to the Dom. - // Removes it if second parameter is true. - function addScript(script, remove) { - var s = document.createElement("script"); - s.textContent = script; - document.head.appendChild(s); - - // Remove internal messages from the DOM - if (remove) { - s.parentNode.removeChild(s); - } - } - - // Handles incoming websocket connections - function handleConnect() { - window.wailsbridge.log("Connected to backend"); - hideReconnectOverlay(); - clearInterval(window.wailsbridge.connectTimer); - window.wailsbridge.websocket.onclose = handleDisconnect; - window.wailsbridge.websocket.onmessage = handleMessage; - window.wailsbridge.connectionState = "connected"; - } - - // Handles websocket disconnects - function handleDisconnect() { - window.wailsbridge.log("Disconnected from backend"); - window.wailsbridge.websocket = null; - window.wailsbridge.connectionState = "disconnected"; - showReconnectOverlay(); - connect(); - } - - // Try to connect to the backend every 300ms (default value). - // Change this value in the main wailsbridge object. - function connect() { - window.wailsbridge.connectTimer = setInterval(function() { - if (window.wailsbridge.websocket == null) { - window.wailsbridge.websocket = new WebSocket(window.wailsbridge.wsURL); - window.wailsbridge.websocket.onopen = handleConnect; - window.wailsbridge.websocket.onerror = function(e) { - e.stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - window.wailsbridge.websocket = null; - return false; - }; - } - }, window.wailsbridge.reconnectTimer); - } - - function handleMessage(message) { - // As a bridge we ignore js and css injections - - switch (message.data[0]) { - // Wails library - inject! - case "w": - addScript(message.data.slice(1)); - - // Now wails runtime is loaded, wails for the ready event - // and callback to the main app - window.wails.events.on("wails:loaded", function() { - window.wailsbridge.log("Wails Ready"); - if (window.wailsbridge.callback) { - window.wailsbridge.log("Notifying application"); - window.wailsbridge.callback(window.wails); - } - }); - window.wailsbridge.log("Loaded Wails Runtime"); - break; - // Notifications - case "n": - addScript(message.data.slice(1), true); - break; - // Binding - case "b": - var binding = message.data.slice(1); - //log("Binding: " + binding) - window.wails._.newBinding(binding); - break; - // Call back - case "c": - var callbackData = message.data.slice(1); - window.wailsbridge.log("Callback = " + callbackData); - window.wails._.callback(callbackData); - break; - } - } - - // Start by showing the overlay... - showReconnectOverlay(); - - // ...and attempt to connect - connect(); -} - -export default { - // The main function - // Passes the main Wails object to the callback if given. - Start: function(callback) { - // Save the callback - window.wailsbridge.callback = callback; - - // Start Bridge - startBridge(); - } -}; diff --git a/wailsruntimeassets/bridge/wailsbridge.prod.js b/wailsruntimeassets/bridge/wailsbridge.prod.js deleted file mode 100644 index 62c723900..000000000 --- a/wailsruntimeassets/bridge/wailsbridge.prod.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - Wails Bridge (c) 2019-present Lea Anthony - - This prod version is to get around having to rewrite your code - for production. When doing a release build, this file will be used - instead of the full version. -*/ - -export default { - // The main function - // Passes the main Wails object to the callback if given. - Start: function(callback) { - if (callback) { - window.wails.events.on("wails:ready", callback); - } - } -}; diff --git a/wailsruntimeassets/default/default.html b/wailsruntimeassets/default/default.html deleted file mode 100644 index 6de192d56..000000000 --- a/wailsruntimeassets/default/default.html +++ /dev/null @@ -1,5 +0,0 @@ - - - -
- \ No newline at end of file diff --git a/wailsruntimeassets/default/wails.css b/wailsruntimeassets/default/wails.css deleted file mode 100644 index b807abe69..000000000 --- a/wailsruntimeassets/default/wails.css +++ /dev/null @@ -1,39 +0,0 @@ -/* - Some css from https://gist.github.com/abelaska/9c9eda70d31315f27a564be2ee490cf4 - Many thanks! -*/ - -/* https://fonts.google.com/specimen/Roboto?selection.family=Roboto:300,400,500,700 https://www.fontsquirrel.com/tools/webfont-generator */ -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 300; - src:url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); -} -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); -} -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); -} -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 700; - src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); -} - -html { - font-family: 'Roboto' -} - -.wails-logo { - background-image: url(data:image/png;base64,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); - background-size: cover; -} \ No newline at end of file diff --git a/wailsruntimeassets/default/wails.min.js b/wailsruntimeassets/default/wails.min.js deleted file mode 100644 index 2c51ff97f..000000000 --- a/wailsruntimeassets/default/wails.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){window.wails=window.wails||{};window.backend={};function cryptoRandom(){var array=new Uint32Array(1);return window.crypto.getRandomValues(array)[0]}function basicRandom(){return Math.random()*9007199254740991}var randomFunc;if(window.crypto){randomFunc=cryptoRandom}else{randomFunc=basicRandom}function isValidIdentifier(name){try{new Function("var "+name);return true}catch(e){return false}}function addScript(js,callbackID){var script=document.createElement("script");script.text=js;document.body.appendChild(script);window.wails.events.emit(callbackID)}function injectCSS(css){var elem=document.createElement('style');elem.setAttribute('type','text/css');if(elem.styleSheet){elem.styleSheet.cssText=css}else{elem.appendChild(document.createTextNode(css))}var head=document.head||document.getElementsByTagName('head')[0];head.appendChild(elem)}var bindingsBasePath=window.backend;function addBindingPath(pathSections){var currentPath=bindingsBasePath;for(var sectionIndex in pathSections){var section=pathSections[sectionIndex];if(!isValidIdentifier(section)){var errMessage=section+" is not a valid javascript identifier.";var err=new Error(errMessage);return[null,err]}if(!currentPath[section]){currentPath[section]={}}currentPath=currentPath[section]}return[currentPath,null]}function newBinding(bindingName){var bindingSections=bindingName.split('.').splice(1);var callName=bindingSections.pop();var pathToBinding;var err;var bs=addBindingPath(bindingSections);var pathToBinding=bs[0];var err=bs[1];if(err!=null){return err}pathToBinding[callName]=function(){var timeout=0;function dynamic(){var args=[].slice.call(arguments);return call(bindingName,args,timeout)}dynamic.setTimeout=function(newTimeout){timeout=newTimeout};dynamic.getTimeout=function(){return timeout};return dynamic}()}var callbacks={};function call(bindingName,data,timeout){if(timeout==null||timeout==undefined){timeout=0}return new Promise(function(resolve,reject){var callbackID;do{callbackID=bindingName+"-"+randomFunc()}while(callbacks[callbackID]);if(timeout>0){var timeoutHandle=setTimeout(function(){reject(Error("Call to "+bindingName+" timed out. Request ID: "+callbackID))},timeout)}callbacks[callbackID]={timeoutHandle:timeoutHandle,reject:reject,resolve:resolve};try{var payloaddata=JSON.stringify(data);message={type:"call",callbackid:callbackID,payload:{bindingName:bindingName,data:payloaddata}};var payload=JSON.stringify(message);external.invoke(payload)}catch(e){console.error(e)}})}function callback(incomingMessage){var message;try{message=JSON.parse(incomingMessage)}catch(e){wails.log.debug("Invalid JSON passed to callback: "+e.message);wails.log.debug("Message: "+incomingMessage);return}callbackID=message.callbackid;callbackData=callbacks[callbackID];if(!callbackData){console.error("Callback '"+callbackID+"' not registed!!!");return}clearTimeout(callbackData.timeoutHandle);delete callbacks[callbackID];if(message.error){return callbackData.reject(message.error)}return callbackData.resolve(message.data)}var eventListeners={};function on(eventName,callback){eventListeners[eventName]=eventListeners[eventName]||[];eventListeners[eventName].push(callback)}function notify(eventName,data){if(eventListeners[eventName]){eventListeners[eventName].forEach(function(element){var parsedData=[];if(data){try{parsedData=JSON.parse(data)}catch(e){wails.log.error("Invalid JSON data sent to notify. Event name = "+eventName)}}element.apply(null,parsedData)})}}function emit(eventName){var data=JSON.stringify([].slice.apply(arguments).slice(1));message={type:"event",payload:{name:eventName,data:data}};external.invoke(JSON.stringify(message))}window.wails.events={emit:emit,on:on};function sendLogMessage(level,message){message={type:"log",payload:{level:level,message:message}};external.invoke(JSON.stringify(message))}function logDebug(message){sendLogMessage("debug",message)}function logInfo(message){sendLogMessage("info",message)}function logWarning(message){sendLogMessage("warning",message)}function logError(message){sendLogMessage("error",message)}function logFatal(message){sendLogMessage("fatal",message)}window.wails.log={debug:logDebug,info:logInfo,warning:logWarning,error:logError,fatal:logFatal};window.wails._={newBinding:newBinding,callback:callback,notify:notify,sendLogMessage:sendLogMessage,callbacks:callbacks,injectCSS:injectCSS,addScript:addScript};window.wails.events.emit("wails:loaded");})(); \ No newline at end of file diff --git a/wailsruntimeassets/wails.js b/wailsruntimeassets/wails.js deleted file mode 100644 index ecaa8c13d..000000000 --- a/wailsruntimeassets/wails.js +++ /dev/null @@ -1,343 +0,0 @@ -// Wails runtime JS -(function () { - window.wails = window.wails || {}; - window.backend = {}; - - /****************** Utility Functions ************************/ - - // -------------- Random -------------- - // AwesomeRandom - function cryptoRandom() { - var array = new Uint32Array(1); - return window.crypto.getRandomValues(array)[0]; - } - - - // LOLRandom - function basicRandom() { - return Math.random() * 9007199254740991; - } - - // Pick one based on browser capability - var randomFunc; - if (window.crypto) { - randomFunc = cryptoRandom; - } else { - randomFunc = basicRandom; - } - - // -------------- Identifiers --------------- - - function isValidIdentifier(name) { - // Don't xss yourself :-) - try { - new Function("var " + name); - return true; - } catch (e) { - return false; - } - } - - // -------------- JS ---------------- - function addScript(js, callbackID) { - var script = document.createElement("script"); - script.text = js; - document.body.appendChild(script); - window.wails.events.emit(callbackID); - } - - // -------------- CSS --------------- - // Adapted from webview - thanks zserge! - function injectCSS(css) { - var elem = document.createElement('style'); - elem.setAttribute('type', 'text/css'); - if (elem.styleSheet) { - elem.styleSheet.cssText = css; - } else { - elem.appendChild(document.createTextNode(css)); - } - var head = document.head || document.getElementsByTagName('head')[0]; - head.appendChild(elem); - } - - /************************* Bindings *************************/ - - var bindingsBasePath = window.backend; - - // Creates the path given in the bindings path - function addBindingPath(pathSections) { - // Start at the base path - var currentPath = bindingsBasePath; - // for each section of the given path - for (var sectionIndex in pathSections) { - - var section = pathSections[sectionIndex]; - - // Is section a valid javascript identifier? - if (!isValidIdentifier(section)) { - var errMessage = section + " is not a valid javascript identifier."; - var err = new Error(errMessage); - return [null, err]; - } - - // Add if doesn't exist - if (!currentPath[section]) { - currentPath[section] = {}; - } - // update current path to new path - currentPath = currentPath[section]; - } - return [currentPath, null]; - } - - function newBinding(bindingName) { - - // Get all the sections of the binding - var bindingSections = bindingName.split('.').splice(1); - - // Get the actual function/method call name - var callName = bindingSections.pop(); - - var pathToBinding; - var err; - - // Add path to binding - var bs = addBindingPath(bindingSections); - var pathToBinding = bs[0]; - var err = bs[1]; - - if (err != null) { - // We need to return an error - return err; - } - - // Add binding call - pathToBinding[callName] = function () { - - // No timeout by default - var timeout = 0; - - // Actual function - function dynamic() { - var args = [].slice.call(arguments); - return call(bindingName, args, timeout); - } - - // Allow setting timeout to function - dynamic.setTimeout = function (newTimeout) { - timeout = newTimeout; - } - - // Allow getting timeout to function - dynamic.getTimeout = function () { - return timeout; - } - - return dynamic; - }(); - } - - /************************************************************/ - - /*************************** Calls **************************/ - - var callbacks = {}; - - // Call sends a message to the backend to call the binding with the - // given data. A promise is returned and will be completed when the - // backend responds. This will be resolved when the call was successful - // or rejected if an error is passed back. - // There is a timeout mechanism. If the call doesn't respond in the given - // time (in milliseconds) then the promise is rejected. - - function call(bindingName, data, timeout) { - - // Timeout infinite by default - if (timeout == null || timeout == undefined) { - timeout = 0; - } - - // Create a promise - return new Promise(function (resolve, reject) { - - // Create a unique callbackID - var callbackID; - do { - callbackID = bindingName + "-" + randomFunc(); - } while (callbacks[callbackID]); - - // Set timeout - if (timeout > 0) { - var timeoutHandle = setTimeout(function () { - reject(Error("Call to " + bindingName + " timed out. Request ID: " + callbackID)); - }, timeout); - } - - // Store callback - callbacks[callbackID] = { - timeoutHandle: timeoutHandle, - reject: reject, - resolve: resolve - } - try { - var payloaddata = JSON.stringify(data); - // Create the message - message = { - type: "call", - callbackid: callbackID, - payload: { - bindingName: bindingName, - data: payloaddata, - } - } - - // Make the call - var payload = JSON.stringify(message); - external.invoke(payload); - } catch (e) { - console.error(e); - } - }) - } - - // Called by the backend to return data to a previously called - // binding invocation - function callback(incomingMessage) { - // Parse the message - var message; - try { - message = JSON.parse(incomingMessage); - } catch (e) { - wails.log.debug("Invalid JSON passed to callback: " + e.message); - wails.log.debug("Message: " + incomingMessage); - return; - } - callbackID = message.callbackid; - callbackData = callbacks[callbackID]; - if (!callbackData) { - console.error("Callback '" + callbackID + "' not registed!!!"); - return - } - clearTimeout(callbackData.timeoutHandle); - delete callbacks[callbackID]; - if (message.error) { - return callbackData.reject(message.error); - } - return callbackData.resolve(message.data); - } - - /************************************************************/ - - - /************************** Events **************************/ - - var eventListeners = {}; - - // Registers event listeners - function on(eventName, callback) { - eventListeners[eventName] = eventListeners[eventName] || []; - eventListeners[eventName].push(callback); - } - - - // notify informs frontend listeners that an event was emitted with the given data - function notify(eventName, data) { - if (eventListeners[eventName]) { - eventListeners[eventName].forEach(function(element) { - var parsedData = []; - // Parse data if we have it - if (data) { - try { - parsedData = JSON.parse(data); - } catch (e) { - wails.log.error("Invalid JSON data sent to notify. Event name = " + eventName) - } - } - element.apply(null, parsedData); - }); - } - } - - // emit an event with the given name and data - function emit(eventName) { - - // Calculate the data - var data = JSON.stringify([].slice.apply(arguments).slice(1)); - - // Notify backend - message = { - type: "event", - payload: { - name: eventName, - data: data, - } - } - external.invoke(JSON.stringify(message)); - } - - // Events calls - window.wails.events = { emit: emit, on: on }; - - - /************************************************************/ - - /************************* Logging **************************/ - - // Sends a log message to the backend with the given - // level + message - function sendLogMessage(level, message) { - - // Log Message - message = { - type: "log", - payload: { - level: level, - message: message, - } - } - external.invoke(JSON.stringify(message)); - } - - function logDebug(message) { - sendLogMessage("debug", message); - } - function logInfo(message) { - sendLogMessage("info", message); - } - function logWarning(message) { - sendLogMessage("warning", message); - } - function logError(message) { - sendLogMessage("error", message); - } - function logFatal(message) { - sendLogMessage("fatal", message); - } - - window.wails.log = { - debug: logDebug, - info: logInfo, - warning: logWarning, - error: logError, - fatal: logFatal, - }; - - /************************** Exports *************************/ - - window.wails._ = { - newBinding: newBinding, - callback: callback, - notify: notify, - sendLogMessage: sendLogMessage, - callbacks: callbacks, - injectCSS: injectCSS, - addScript: addScript, - } - - - /************************************************************/ - - // Notify backend that the runtime has finished loading - window.wails.events.emit("wails:loaded"); - -})() \ No newline at end of file diff --git a/website/.gitattributes b/website/.gitattributes new file mode 100644 index 000000000..65ac44a9e --- /dev/null +++ b/website/.gitattributes @@ -0,0 +1,3 @@ +# Mark all files as documentation so it gets excluded from github language statistics +# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#documentation +** linguist-documentation diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 000000000..a4774bce0 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,21 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.task +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/website/.nvmrc b/website/.nvmrc new file mode 100644 index 000000000..741b4916e --- /dev/null +++ b/website/.nvmrc @@ -0,0 +1 @@ +18.14.0 \ No newline at end of file diff --git a/website/.prettierignore b/website/.prettierignore new file mode 100644 index 000000000..124df96de --- /dev/null +++ b/website/.prettierignore @@ -0,0 +1,2 @@ +i18n +versioned_docs \ No newline at end of file diff --git a/website/.vscode/extensions.json b/website/.vscode/extensions.json new file mode 100644 index 000000000..3433c01b0 --- /dev/null +++ b/website/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode" + ] +} \ No newline at end of file diff --git a/website/.vscode/settings.json b/website/.vscode/settings.json new file mode 100644 index 000000000..f4441702d --- /dev/null +++ b/website/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "[mdx]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, +} \ No newline at end of file diff --git a/website/README.md b/website/README.md new file mode 100644 index 000000000..5194c28d2 --- /dev/null +++ b/website/README.md @@ -0,0 +1,53 @@ +# Website + +This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ npm +``` + +### Local Development + +``` +$ npm run start +``` + +Other languages: + +``` +npm run start -- --locale +``` + +language - The language code configured in the i18n field in the docusaurus.config.js file. + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Translate + +After the English source file is updated, run the following command to submit the source file to Crowdin: + +``` +npm run crowdin push -- -b +``` + +branch - Branch name in crowdin project + +Run the following command to pull the translated files in crowdin to the local: + +``` +npm run crowdin pull -- -b -l +``` + +languageCode - **Note** that this refers to the language code in the crowdin project. + +The recommended practice is to update the English source file locally, then translate the file in crowdin, and finally pull the translated file to the local. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. diff --git a/website/Taskfile.yml b/website/Taskfile.yml new file mode 100644 index 000000000..dbb09105d --- /dev/null +++ b/website/Taskfile.yml @@ -0,0 +1,55 @@ +# https://taskfile.dev + +version: "3" + +tasks: + install: + desc: Install Dependencies + aliases: [i] + cmds: + - corepack enable + - corepack prepare pnpm@8.3.1 --activate + - pnpm install + sources: + - package.json + - pnpm-lock.yaml + + default: + desc: Start Website + deps: [install] + aliases: [s, start] + cmds: + - npx docusaurus start + + build: + desc: Build Website + deps: [install] + cmds: + - npx docusaurus build + + preview: + desc: Preview Website + deps: [build] + aliases: [serve] + cmds: + - npx docusaurus serve + + crowdin:push: + desc: Upload source files to Crowdin + deps: [install] + cmds: + - npx crowdin push -b v2 + + crowdin:pull: + desc: Download approved translation files from Crowdin to local + deps: [install] + cmds: + - npx crowdin pull -b v2 --export-only-approved + + format:md: + cmds: + - npx prettier --write "**/*.{md,mdx}" + + format: + cmds: + - task: format:md diff --git a/website/babel.config.js b/website/babel.config.js new file mode 100644 index 000000000..bfd75dbdf --- /dev/null +++ b/website/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve("@docusaurus/core/lib/babel/preset")], +}; diff --git a/website/blog/2021-09-27-v2-beta1-release-notes.mdx b/website/blog/2021-09-27-v2-beta1-release-notes.mdx new file mode 100644 index 000000000..62466176f --- /dev/null +++ b/website/blog/2021-09-27-v2-beta1-release-notes.mdx @@ -0,0 +1,215 @@ +--- +slug: wails-v2-beta-for-windows +title: Wails v2 Beta for Windows +authors: [leaanthony] +tags: [wails, v2] +--- + +```mdx-code-block +
+ +
+
+``` + +When I first announced Wails on Reddit, just over 2 years ago from a train in Sydney, I did not expect it to get much +attention. A few days later, a prolific tech vlogger released a tutorial video, gave it a positive review and from that +point on, interest in the project has skyrocketed. + +It was clear that people were excited about adding web frontends to their Go projects, and almost immediately pushed the +project beyond the proof of concept that I had created. +At the time, Wails used the [webview](https://github.com/webview/webview) project to handle the frontend, and the only +option for Windows was the IE11 renderer. Many bug reports were rooted in this limitation: poor JavaScript/CSS support +and no dev tools to debug it. This was a frustrating development experience but there wasn't much that could have been +done to rectify it. + +For a long time, I'd firmly believed that Microsoft would eventually have to sort out their browser situation. +The world was moving on, frontend development was booming and IE wasn't cutting it. +When Microsoft announced the move to using Chromium as the basis for their new browser direction, I knew it was only a +matter of time until Wails could use it, and move the Windows developer experience to the next level. + +Today, I am pleased to announce: **Wails v2 Beta for Windows**! There's a huge amount to unpack in this release, so +grab a drink, take a seat and we'll begin... + +### No CGO Dependency! + +No, I'm not joking: _No_ _CGO_ _dependency_ 🤯! The thing about Windows is that, unlike MacOS and Linux, it doesn't +come with a default compiler. In addition, CGO requires a mingw compiler and there's a ton of different installation +options. Removing the CGO requirement has massively simplified setup, as well as making debugging an awful lot easier. +Whilst I have put a fair bit of effort in getting this working, the majority of the +credit should go to [John Chadwick](https://github.com/jchv) for not only starting a couple of projects to make this +possible, but also being open to someone taking those projects and building on them. Credit also to +[Tad Vizbaras](https://github.com/tadvi) whose [winc](https://github.com/tadvi/winc) project started me down this path. + +### WebView2 Chromium Renderer + +```mdx-code-block +
+ +
+
+``` + +Finally, Windows developers get a first class rendering engine for their applications! Gone are the days of contorting +your frontend code to work on Windows. On top of that, you get a first-class developer tools experience! + +The WebView2 component does, however, have a requirement to have the `WebView2Loader.dll` sitting alongside the binary. +This makes distribution just that little bit more painful than we gophers are used to. All solutions and libraries +(that I know of) that use WebView2 have this dependency. + +However, I'm really excited to announce that Wails applications _have no such requirement_! Thanks to the wizardry of +[John Chadwick](https://github.com/jchv), we are able to bundle this dll inside the binary and get Windows to load it +as if it were present on disk. + +Gophers rejoice! The single binary dream lives on! + +### New Features + +```mdx-code-block +
+ +
+
+``` + +There were a lot of requests for native menu support. Wails has finally got you covered. Application menus are now available +and include support for most native menu features. This includes standard menu items, checkboxes, radio groups, submenus +and separators. + +There were a huge number of requests in v1 for the ability to have greater control of the window itself. +I'm happy to announce that there's new runtime APIs specifically for this. +It's feature-rich and supports multi-monitor configurations. There is also an improved dialogs API: Now, you can have modern, native +dialogs with rich configuration to cater for all your dialog needs. + +There is now the option to generate IDE configuration along with your project. This means that if you open your project +in a supported IDE, it will already be configured for building and debugging your application. Currently VSCode is supported +but we hope to support other IDEs such as Goland soon. + +```mdx-code-block +
+ +
+
+``` + +### No requirement to bundle assets + +A huge pain-point of v1 was the need to condense your entire application down to single JS & CSS files. I'm happy to +announce that for v2, there is no requirement to bundle assets, in any way, shape or form. Want to load a local image? Use an +`` tag with a local src path. Want to use a cool font? Copy it in and add the path to it in your CSS. + +> Wow, that sounds like a webserver... + +Yes, it works just like a webserver, except it isn't. + +> So how do I include my assets? + +You just pass a single `embed.FS` that contains all your assets into your application configuration. +They don't even need to be in the top directory - Wails will just work it out for you. + +### New Development Experience + +```mdx-code-block +
+ +
+
+``` + +Now that assets don't need to be bundled, it's enabled a whole new development experience. The new `wails dev` +command will build and run your application, but instead of using the assets in the `embed.FS`, it loads them directly +from disk. + +It also provides the additional features: + +- Hot reload - Any changes to frontend assets will trigger and auto reload of the application frontend +- Auto rebuild - Any changes to your Go code will rebuild and relaunch your application + +In addition to this, a webserver will start on port 34115. This will serve your application to any browser that +connects to it. All connected web browsers will respond to system events like hot reload on asset change. + +In Go, we are used to dealing with structs in our applications. It's often useful to send structs to our frontend +and use them as state in our application. In v1, this was a very manual process and a bit of a burden on the +developer. I'm happy to announce that in v2, any application run in dev mode will automatically generate TypeScript +models for all structs that are input or output parameters to bound methods. This enables seamless interchange of data +models between the two worlds. + +In addition to this, another JS module is dynamically generated wrapping all your bound methods. This provides +JSDoc for your methods, providing code completion and hinting in your IDE. It's really cool when you get data models +auto-imported when hitting tab in an auto-generated module wrapping your Go code! + +### Remote Templates + +```mdx-code-block +
+ +
+
+``` + +Getting an application up and running quickly was always a key goal for the Wails project. When we launched, we tried +to cover a lot of the modern frameworks at the time: react, vue and angular. The world of frontend development is very +opinionated, fast moving and hard to keep on top of! As a result, we found our base templates getting out of date pretty +quickly and this caused a maintenance headache. It also meant that we didn't have cool modern templates for the latest +and greatest tech stacks. + +With v2, I wanted to empower the community by giving you the ability to create and host templates yourselves, rather +than rely on the Wails project. So now you can create projects using community supported templates! I hope this will +inspire developers to create a vibrant ecosystem of project templates. I'm really quite excited about what our developer +community can create! + +### In Conclusion + +Wails v2 represents a new foundation for the project. +The aim of this release is to get feedback on the new approach, and to iron out any bugs before a full release. +Your input would be most welcome. Please direct any feedback to the [v2 Beta](https://github.com/wailsapp/wails/discussions/828) +discussion board. + +There were many twists and turns, pivots and u-turns to get to this point. This was due partly to early technical decisions +that needed changing, and partly because some core problems we had spent time building workarounds for were fixed upstream: +Go’s embed feature is a good example. Fortunately, everything came together at the right time, and today we have the +very best solution that we can have. I believe the wait has been worth it - this would not have been possible even 2 +months ago. + +I also need to give a huge thank you :pray: to the following people because without them, this release just wouldn't exist: + +- [Misite Bao](https://github.com/misitebao) - An absolute workhorse on the Chinese translations and an incredible bug finder. +- [John Chadwick](https://github.com/jchv) - His amazing work on [go-webview2](https://github.com/jchv/go-webview2) and + [go-winloader](https://github.com/jchv/go-winloader) have made the Windows version we have today possible. +- [Tad Vizbaras](https://github.com/tadvi) - Experimenting with his [winc](https://github.com/tadvi/winc) project was the first step down the path to a pure Go Wails. +- [Mat Ryer](https://github.com/matryer) - His support, encouragement and feedback has really helped drive the project forward. + +And finally, I'd like to give a special thank you to all the [project sponsors](/credits#sponsors), including [JetBrains](https://www.jetbrains.com?from=Wails), +whose support drive the project in many ways behind the scenes. + +I look forward to seeing what people build with Wails in this next exciting phase of the project! + +Lea. + +PS: MacOS and Linux users need not feel left out - porting to this new foundation is actively under way and most of the hard work has already been done. Hang in there! + +PPS: If you or your company find Wails useful, please consider [sponsoring the project](https://github.com/sponsors/leaanthony). Thanks! diff --git a/website/blog/2021-11-08-v2-beta2-release-notes.mdx b/website/blog/2021-11-08-v2-beta2-release-notes.mdx new file mode 100644 index 000000000..2f8751c9c --- /dev/null +++ b/website/blog/2021-11-08-v2-beta2-release-notes.mdx @@ -0,0 +1,201 @@ +--- +slug: wails-v2-beta-for-mac +title: Wails v2 Beta for MacOS +authors: [leaanthony] +tags: [wails, v2] +--- + +```mdx-code-block +
+ +
+
+``` + +Today marks the first beta release of Wails v2 for Mac! It's taken quite a while to get to this point and I'm hoping +that today's release will give you something that's reasonably useful. There have been a number of twists and turns +to get to this point and I'm hoping, with your help, to iron out the crinkles and get the Mac port polished for the +final v2 release. + +You mean this isn't ready for production? For your use case, it may well be ready, but there are still a number of +known issues so keep your eye on [this project board](https://github.com/wailsapp/wails/projects/7) and if you would +like to contribute, you'd be very welcome! + +So what's new for Wails v2 for Mac vs v1? Hint: It's pretty similar to the Windows Beta :wink: + +### New Features + +```mdx-code-block +
+ +
+
+``` + +There were a lot of requests for native menu support. Wails has finally got you covered. Application menus are now available +and include support for most native menu features. This includes standard menu items, checkboxes, radio groups, submenus +and separators. + +There were a huge number of requests in v1 for the ability to have greater control of the window itself. +I'm happy to announce that there's new runtime APIs specifically for this. +It's feature-rich and supports multi-monitor configurations. There is also an improved dialogs API: Now, you can have modern, native +dialogs with rich configuration to cater for all your dialog needs. + +### Mac Specific Options + +In addition to the normal application options, Wails v2 for Mac also brings some Mac extras: + +- Make your window all funky and translucent, like all the pretty swift apps! +- Highly customisable titlebar +- We support the NSAppearance options for the application +- Simple config to auto-create an "About" menu + +### No requirement to bundle assets + +A huge pain-point of v1 was the need to condense your entire application down to single JS & CSS files. I'm happy to +announce that for v2, there is no requirement to bundle assets, in any way, shape or form. Want to load a local image? Use an +`` tag with a local src path. Want to use a cool font? Copy it in and add the path to it in your CSS. + +> Wow, that sounds like a webserver... + +Yes, it works just like a webserver, except it isn't. + +> So how do I include my assets? + +You just pass a single `embed.FS` that contains all your assets into your application configuration. +They don't even need to be in the top directory - Wails will just work it out for you. + +### New Development Experience + +Now that assets don't need to be bundled, it's enabled a whole new development experience. The new `wails dev` +command will build and run your application, but instead of using the assets in the `embed.FS`, it loads them directly +from disk. + +It also provides the additional features: + +- Hot reload - Any changes to frontend assets will trigger and auto reload of the application frontend +- Auto rebuild - Any changes to your Go code will rebuild and relaunch your application + +In addition to this, a webserver will start on port 34115. This will serve your application to any browser that +connects to it. All connected web browsers will respond to system events like hot reload on asset change. + +In Go, we are used to dealing with structs in our applications. It's often useful to send structs to our frontend +and use them as state in our application. In v1, this was a very manual process and a bit of a burden on the +developer. I'm happy to announce that in v2, any application run in dev mode will automatically generate TypeScript +models for all structs that are input or output parameters to bound methods. This enables seamless interchange of data +models between the two worlds. + +In addition to this, another JS module is dynamically generated wrapping all your bound methods. This provides +JSDoc for your methods, providing code completion and hinting in your IDE. It's really cool when you get data models +auto-imported when hitting tab in an auto-generated module wrapping your Go code! + +### Remote Templates + +```mdx-code-block +
+ +
+
+``` + +Getting an application up and running quickly was always a key goal for the Wails project. When we launched, we tried +to cover a lot of the modern frameworks at the time: react, vue and angular. The world of frontend development is very +opinionated, fast moving and hard to keep on top of! As a result, we found our base templates getting out of date pretty +quickly and this caused a maintenance headache. It also meant that we didn't have cool modern templates for the latest +and greatest tech stacks. + +With v2, I wanted to empower the community by giving you the ability to create and host templates yourselves, rather +than rely on the Wails project. So now you can create projects using community supported templates! I hope this will +inspire developers to create a vibrant ecosystem of project templates. I'm really quite excited about what our developer +community can create! + +### Native M1 Support + +Thanks to the amazing support of [Mat Ryer](https://github.com/matryer/), the Wails project now supports M1 native +builds: + +```mdx-code-block +
+ +
+
+``` + +You can also specify `darwin/amd64` as a target too: + +```mdx-code-block +
+ +
+
+``` + +Oh, I almost forgot.... you can also do `darwin/universal`.... :wink: + +```mdx-code-block +
+ +
+
+``` + +### Cross Compilation to Windows + +Because Wails v2 for Windows is pure Go, you can target Windows builds without docker. + +```mdx-code-block +
+ +
+
+``` + +### WKWebView Renderer + +V1 relied on a (now deprecated) WebView component. V2 uses the most recent WKWebKit component so expect the latest and greatest from Apple. + +### In Conclusion + +As I'd said in the Windows release notes, Wails v2 represents a new foundation for the project. +The aim of this release is to get feedback on the new approach, and to iron out any bugs before a full release. +Your input would be most welcome! Please direct any feedback to the [v2 Beta](https://github.com/wailsapp/wails/discussions/828) +discussion board. + +And finally, I'd like to give a special thank you to all the [project sponsors](/credits#sponsors), including [JetBrains](https://www.jetbrains.com?from=Wails), +whose support drive the project in many ways behind the scenes. + +I look forward to seeing what people build with Wails in this next exciting phase of the project! + +Lea. + +PS: Linux users, you're next! + +PPS: If you or your company find Wails useful, please consider [sponsoring the project](https://github.com/sponsors/leaanthony). Thanks! diff --git a/website/blog/2022-02-22-v2-beta3-release-notes.mdx b/website/blog/2022-02-22-v2-beta3-release-notes.mdx new file mode 100644 index 000000000..1471ec1d1 --- /dev/null +++ b/website/blog/2022-02-22-v2-beta3-release-notes.mdx @@ -0,0 +1,142 @@ +--- +slug: wails-v2-beta-for-linux +title: Wails v2 Beta for Linux +authors: [leaanthony] +tags: [wails, v2] +--- + +```mdx-code-block +
+ +
+
+``` + +I'm pleased to finally announce that Wails v2 is now in beta for Linux! It is somewhat ironic that the very first +experiments with v2 was on Linux and yet it has ended up as the last release. That being said, the v2 we have today +is very different from those first experiments. So without further ado, let's go over the new features: + +### New Features + +```mdx-code-block +
+ +
+
+``` + +There were a lot of requests for native menu support. Wails has finally got you covered. Application menus are now available +and include support for most native menu features. This includes standard menu items, checkboxes, radio groups, submenus +and separators. + +There were a huge number of requests in v1 for the ability to have greater control of the window itself. +I'm happy to announce that there's new runtime APIs specifically for this. +It's feature-rich and supports multi-monitor configurations. There is also an improved dialogs API: Now, you can have modern, native +dialogs with rich configuration to cater for all your dialog needs. + +### No requirement to bundle assets + +A huge pain-point of v1 was the need to condense your entire application down to single JS & CSS files. I'm happy to +announce that for v2, there is no requirement to bundle assets, in any way, shape or form. Want to load a local image? Use an +`` tag with a local src path. Want to use a cool font? Copy it in and add the path to it in your CSS. + +> Wow, that sounds like a webserver... + +Yes, it works just like a webserver, except it isn't. + +> So how do I include my assets? + +You just pass a single `embed.FS` that contains all your assets into your application configuration. +They don't even need to be in the top directory - Wails will just work it out for you. + +### New Development Experience + +Now that assets don't need to be bundled, it's enabled a whole new development experience. The new `wails dev` +command will build and run your application, but instead of using the assets in the `embed.FS`, it loads them directly +from disk. + +It also provides the additional features: + +- Hot reload - Any changes to frontend assets will trigger and auto reload of the application frontend +- Auto rebuild - Any changes to your Go code will rebuild and relaunch your application + +In addition to this, a webserver will start on port 34115. This will serve your application to any browser that +connects to it. All connected web browsers will respond to system events like hot reload on asset change. + +In Go, we are used to dealing with structs in our applications. It's often useful to send structs to our frontend +and use them as state in our application. In v1, this was a very manual process and a bit of a burden on the +developer. I'm happy to announce that in v2, any application run in dev mode will automatically generate TypeScript +models for all structs that are input or output parameters to bound methods. This enables seamless interchange of data +models between the two worlds. + +In addition to this, another JS module is dynamically generated wrapping all your bound methods. This provides +JSDoc for your methods, providing code completion and hinting in your IDE. It's really cool when you get data models +auto-imported when hitting tab in an auto-generated module wrapping your Go code! + +### Remote Templates + +```mdx-code-block +
+ +
+
+``` + +Getting an application up and running quickly was always a key goal for the Wails project. When we launched, we tried +to cover a lot of the modern frameworks at the time: react, vue and angular. The world of frontend development is very +opinionated, fast moving and hard to keep on top of! As a result, we found our base templates getting out of date pretty +quickly and this caused a maintenance headache. It also meant that we didn't have cool modern templates for the latest +and greatest tech stacks. + +With v2, I wanted to empower the community by giving you the ability to create and host templates yourselves, rather +than rely on the Wails project. So now you can create projects using community supported templates! I hope this will +inspire developers to create a vibrant ecosystem of project templates. I'm really quite excited about what our developer +community can create! + +### Cross Compilation to Windows + +Because Wails v2 for Windows is pure Go, you can target Windows builds without docker. + +```mdx-code-block +
+ +
+
+``` + +### In Conclusion + +As I'd said in the Windows release notes, Wails v2 represents a new foundation for the project. +The aim of this release is to get feedback on the new approach, and to iron out any bugs before a full release. +Your input would be most welcome! Please direct any feedback to the [v2 Beta](https://github.com/wailsapp/wails/discussions/828) +discussion board. + +Linux is **hard** to support. We expect there to be a number of quirks with the beta. Please help us to help you by +filing detailed bug reports! + +Finally, I'd like to give a special thank you to all the [project sponsors](/credits#sponsors) whose support +drive the project in many ways behind the scenes. + +I look forward to seeing what people build with Wails in this next exciting phase of the project! + +Lea. + +PS: The v2 release isn't far off now! + +PPS: If you or your company find Wails useful, please consider [sponsoring the project](https://github.com/sponsors/leaanthony). Thanks! diff --git a/website/blog/2022-09-22-v2-release-notes.mdx b/website/blog/2022-09-22-v2-release-notes.mdx new file mode 100644 index 000000000..bcf972450 --- /dev/null +++ b/website/blog/2022-09-22-v2-release-notes.mdx @@ -0,0 +1,96 @@ +--- +slug: wails-v2-released +title: Wails v2 Released +authors: [leaanthony] +tags: [wails, v2] +--- + +```mdx-code-block +
+ +
+
+``` + +# It's here! + +Today marks the release of [Wails](https://wails.io) v2. It's been about 18 months since the first v2 alpha and about a year from the first beta release. I'm truly grateful to everyone involved in the evolution of the project. + +Part of the reason it took that long was due to wanting to get to some definition of completeness before officially calling it v2. The truth is, there's never a perfect time to tag a release - there's always outstanding issues or "just one more" feature to squeeze in. What tagging an imperfect major release does do, however, is to provide a bit of stability for users of the project, as well as a bit of a reset for the developers. + +This release is more than I'd ever expected it to be. I hope it gives you as much pleasure as it has given us to develop it. + +# What _is_ Wails? + +If you are unfamiliar with Wails, it is a project that enables Go programmers to provide rich frontends for their Go programs using familiar web technologies. It's a lightweight, Go alternative to Electron. Much more information can be found on the [official site](https://wails.io/docs/introduction). + +# What's new? + +The v2 release is a huge leap forward for the project, addressing many of the pain points of v1. If you have not read any of the blog posts on the Beta releases for [macOS](/blog/wails-v2-beta-for-mac), [Windows](/blog/wails-v2-beta-for-windows) or [Linux](/blog/wails-v2-beta-for-linux), then I encourage you to do so +as it covers all the major changes in more detail. In summary: + +- Webview2 component for Windows that supports modern web standards and debugging capabilities. +- [Dark / Light theme](/docs/reference/options#theme) + [custom theming](/docs/reference/options#customtheme) on Windows. +- Windows now has no CGO requirements. +- Out-of-the-box support for Svelte, Vue, React, Preact, Lit & Vanilla project templates. +- [Vite](https://vitejs.dev/) integration providing a hot-reload development environment for your application. +- Native application [menus](/docs/guides/application-development#application-menu) and [dialogs](/docs/reference/runtime/dialog). +- Native window translucency effects for [Windows](/docs/reference/options#windowistranslucent) and [macOS](/docs/reference/options#windowistranslucent-1). Support for Mica & Acrylic backdrops. +- Easily generate an [NSIS installer](/docs/guides/windows-installer) for Windows deployments. +- A rich [runtime library](/docs/reference/runtime/intro) providing utility methods for window manipulation, eventing, dialogs, menus and logging. +- Support for [obfuscating](/docs/guides/obfuscated) your application using [garble](https://github.com/burrowers/garble). +- Support for compressing your application using [UPX](https://upx.github.io/). +- Automatic TypeScript generation of Go structs. More info [here](/docs/howdoesitwork#calling-bound-go-methods). +- No extra libraries or DLLs are required to be shipped with your application. For any platform. +- No requirement to bundle frontend assets. Just develop your application like any other web application. + +# Credit & Thanks + +Getting to v2 has been a huge effort. There have been ~2.2K commits by 89 contributors between the initial alpha and the release today, and many, many more that have provided translations, testing, feedback and help on the discussion forums as well as the issue tracker. I'm so unbelievably grateful to each one of you. I'd also like to give an extra special thank you to all the project sponsors who have provided guidance, advice and feedback. Everything you do is hugely appreciated. + +There are a few people I'd like to give special mention to: + +Firstly, a **huge** thank you to [@stffabi](https://github.com/stffabi) who has provided so many contributions which we all benefit from, as well as providing a lot of support on many issues. He has provided some key features such as the external dev server support which transformed our dev mode offering by allowing us to hook into [Vite](https://vitejs.dev/)'s superpowers. It's fair to say that Wails v2 would be a far less exciting release without his [incredible contributions](https://github.com/wailsapp/wails/commits?author=stffabi&since=2020-01-04). Thank you so much @stffabi! + +I'd also like to give a huge shout-out to [@misitebao](https://github.com/misitebao) who has tirelessly been maintaining the website, as well as providing Chinese translations, managing Crowdin and helping new translators get up to speed. This is a hugely important task, and I'm extremely grateful for all the time and effort put into this! You rock! + +Last, but not least, a huge thank you to Mat Ryer who has provided advice and support during the development of v2. Writing xBar together using an early Alpha of v2 was helpful in shaping the direction of v2, as well as give me an understanding of some design flaws in the early releases. I'm happy to announce that as of today, we will start to port xBar to Wails v2, and it will become the flagship application for the project. Cheers Mat! + +# Lessons Learnt + +There are a number of lessons learnt in getting to v2 that will shape development moving forward. + +## Smaller, Quicker, Focused Releases + +In the course of developing v2, there were many features and bug fixes that were developed on an ad-hoc basis. This led to longer release cycles and were harder to debug. Moving forward, we are going to create releases more often that will include a reduced number of features. A release will involve updates to documentation as well as thorough testing. Hopefully, these smaller, quicker, focussed releases will lead to fewer regressions and better quality documentation. + +## Encourage Engagement + +When starting this project, I wanted to immediately help everyone who had a problem. Issues were "personal" and I wanted them resolved as quickly as possible. This is unsustainable and ultimately works against the longevity of the project. Moving forward, I will be giving more space for people to get involved in answering questions and triaging issues. It would be good to get some tooling to help with this so if you have any suggestions, please join in the discussion [here](https://github.com/wailsapp/wails/discussions/1855). + +## Learning to say No + +The more people that engage with an Open Source project, the more requests there will be for additional features that may or may not be useful to the majority of people. These features will take an initial amount of time to develop and debug, and incur an ongoing maintenance cost from that point on. I myself am the most guilty of this, often wanting to "boil the sea" rather than provide the minimum viable feature. Moving forward, we will need to say "No" a bit more to adding core features and focus our energies on a way to empower developers to provide that functionality themselves. We are looking seriously into plugins for this scenario. This will allow anyone to extend the project as they see fit, as well as providing an easy way to contribute towards the project. + +# Looking to the Future + +There are so many core features we are looking at to add to Wails in the next major development cycle already. The [roadmap](https://github.com/wailsapp/wails/discussions/1484) is full of interesting ideas, and I'm keen to start work on them. One of the big asks has been for multiple window support. It's a tricky one and to do it right, and we may need to look at providing an alternative API, as the current one was not designed with this in mind. Based on some preliminary ideas and feedback, I think you'll like where we're looking to go with it. + +I'm personally very excited at the prospect of getting Wails apps running on mobile. We already have a demo project showing that it is possible to run a Wails app on Android, so I'm really keen to explore where we can go with this! + +A final point I'd like to raise is that of feature parity. It has long been a core principle that we wouldn't add anything to the project without there being full cross-platform support for it. Whilst this has proven to be (mainly) achievable so far, it has really held the project back in releasing new features. Moving forward, we will be adopting a slightly different approach: any new feature that cannot be immediately released for all platforms will be released under an experimental configuration or API. This allows early adopters on certain platforms to try the feature and provide feedback that will feed into the final design of the feature. This, of course, means that there are no guarantees of API stability until it is fully supported by all the platforms it can be supported on, but at least it will unblock development. + +# Final Words + +I'm really proud of what we've been able to achieve with the V2 release. It's amazing to see what people have already been able to build using the beta releases so far. Quality applications like [Varly](https://varly.app/), [Surge](https://getsurge.io/) and [October](https://october.utf9k.net/). I encourage you to check them out. + +This release was achieved through the hard work of many contributors. Whilst it is free to download and use, it has not come about through zero cost. Make no mistakes, this project has come at considerable cost. It has not only been my time and the time of each and every contributor, but also the cost of absence from friends and families of each of those people too. That's why I'm extremely grateful for every second that has been dedicated to making this project happen. The more contributors we have, the more this effort can be spread out and the more we can achieve together. I'd like to encourage you all to pick one thing that you can contribute, whether it is confirming someone's bug, suggesting a fix, making a documentation change or helping out someone who needs it. All of these small things have such a huge impact! It would be so awesome if you too were part of the story in getting to v3. + +Enjoy! + +‐ Lea + +PS: If you or your company find Wails useful, please consider [sponsoring the project](https://github.com/sponsors/leaanthony). Thanks! diff --git a/website/blog/2023-01-17-v3-roadmap.mdx b/website/blog/2023-01-17-v3-roadmap.mdx new file mode 100644 index 000000000..adc53e36e --- /dev/null +++ b/website/blog/2023-01-17-v3-roadmap.mdx @@ -0,0 +1,240 @@ +--- +slug: the-road-to-wails-v3 +title: The Road to Wails v3 +authors: [leaanthony] +tags: [wails, v3] +--- + +```mdx-code-block +
+ +
+
+``` + +# Introduction + +Wails is a project that simplifies the ability to write cross-platform desktop applications using +Go. It uses native webview components for the frontend (not embedded browsers), bringing the power +of the world's most popular UI system to Go, whilst remaining lightweight. + +Version 2 was released on the 22nd of September 2022 and brought with it a lot of enhancements +including: + +- Live development, leveraging the popular Vite project +- Rich functionality for managing windows and creating menus +- Microsoft's WebView2 component +- Generation of Typescript models that mirror your Go structs +- Creating of NSIS Installer +- Obfuscated builds + +Right now, Wails v2 provides powerful tooling for creating rich, cross-platform desktop +applications. + +This blog post aims to look at where the project is at right now and what we can improve +on moving forward. + +# Where are we now? + +It's been incredible to see the popularity of Wails rising since the v2 release. I'm constantly +amazed by the creativity of the community and the wonderful things that are being built with it. +With more popularity, comes more eyes on the project. And with that, more feature requests and +bug reports. + +Over time, I've been able to identify some of the most pressing issues facing the project. +I've also been able to identify some of the things that are holding the project back. + +## Current issues + +I've identified the following areas that I feel are holding the project back: + +- The API +- Bindings generation +- The Build System + +### The API + +The API to build a Wails application currently consists of 2 parts: + +- The Application API +- The Runtime API + +The Application API famously has only 1 function: `Run()` which takes a heap of +options which govern how the application will work. Whilst this is very simple to use, +it is also very limiting. It is a "declarative" approach which hides a lot of the +underlying complexity. For instance, there is no handle to the main window, so you can't +interact with it directly. For that, you need to use the Runtime API. This is a problem +when you start to want to do more complex things like create multiple windows. + +The Runtime API provides a lot of utility functions for the developer. This includes: + +- Window management +- Dialogs +- Menus +- Events +- Logs + +There are a number of things I am not happy with the Runtime API. The first is that it +requires a "context" to be passed around. This is both frustrating and confusing for +new developers who pass in a context and then get a runtime error. + +The biggest issue with the Runtime API is that it was designed for applications that only +use a single window. Over time, the demand for multiple windows has grown and the API is +not well suited to this. + +### Thoughts on the v3 API + +Wouldn't it be great if we could do something like this? + +```go +func main() { + app := wails.NewApplication(options.App{}) + myWindow := app.NewWindow(options.Window{}) + myWindow.SetTitle("My Window") + myWindow.On(events.Window.Close, func() { + app.Quit() + }) + app.Run() +} +``` + +This programmatic approach is far more intuitive and allows the developer to interact +with the application elements directly. All current runtime methods for windows would +simply be methods on the window object. For the other runtime methods, we could move +them to the application object like so: + +```go +app := wails.NewApplication(options.App{}) +app.NewInfoDialog(options.InfoDialog{}) +app.Log.Info("Hello World") +``` + +This is a much more powerful API which will allow for more complex applications to be built. +It also allows for the creation of multiple windows, [the most up-voted feature on GitHub](https://github.com/wailsapp/wails/issues/1480): + +```go +func main() { + app := wails.NewApplication(options.App{}) + myWindow := app.NewWindow(options.Window{}) + myWindow.SetTitle("My Window") + myWindow.On(events.Window.Close, func() { + app.Quit() + }) + myWindow2 := app.NewWindow(options.Window{}) + myWindow2.SetTitle("My Window 2") + myWindow2.On(events.Window.Close, func() { + app.Quit() + }) + app.Run() +} +``` + +### Bindings generation + +One of the key features of Wails is generating bindings for your Go methods so they may be +called from Javascript. The current method for doing this is a bit of a hack. It involves +building the application with a special flag and then running the resultant binary which +uses reflection to determine what has been bound. This leads to a bit of a chicken and egg +situation: You can't build the application without the bindings and you can't generate the +bindings without building the application. There are many ways around this but the best one +would be not to use this approach at all. + +There was a number of attempts at writing a static analyser for Wails projects but they +didn't get very far. In more recent times, it has become slightly easier to do this with +more material available on the subject. + +Compared to reflection, the AST approach is much faster however it is significantly more +complicated. To start with, we may need to impose certain constraints on how to specify +bindings in the code. The goal is to support the most common use cases and then expand +it later on. + +### The Build System + +Like the declarative approach to the API, the build system was created to hide the +complexities of building a desktop application. When you run `wails build`, it does a +lot of things behind the scenes: +- Builds the backend binary for bindings and generates the bindings +- Installs the frontend dependencies +- Builds the frontend assets +- Determines if the application icon is present and if so, embeds it +- Builds the final binary +- If the build is for `darwin/universal` it builds 2 binaries, one for `darwin/amd64` and one for `darwin/arm64` and then creates a fat binary using `lipo` +- If compression is required, it compresses the binary with UPX +- Determines if this binary is to be packaged and if so: + - Ensures the icon and application manifest are compiled into the binary (Windows) + - Builds out the application bundle, generates the icon bundle and copies it, the binary and Info.plist to the application bundle (Mac) +- If an NSIS installer is required, it builds it + +This entire process, whilst very powerful, is also very opaque. It is very difficult to +customise it and it is very difficult to debug. + +To address this in v3, I would like to move to a build system that exists outside of Wails. +After using [Task](https://taskfile.dev/) for a while, I am a big fan of it. It is a great +tool for configuring build systems and should be reasonably familiar to anyone who has used +Makefiles. + +The build system would be configured using a `Taskfile.yml` file which would be generated +by default with any of the supported templates. This would have all of the steps required +to do all the current tasks, such as building or packaging the application, allowing for +easy customisation. + +There will be no external requirement for this tooling as it would form part of the Wails +CLI. This means that you can still use `wails build` and it will do all the things it does +today. However, if you want to customise the build process, you can do so by editing the +`Taskfile.yml` file. It also means you can easily understand the build steps and use your +own build system if you wish. + +The missing piece in the build puzzle is the atomic operations in the build process, such +as icon generation, compression and packaging. To require a bunch of external tooling would +not be a great experience for the developer. To address this, the Wails CLI will provide +all these capabilities as part of the CLI. This means that the builds still work as expected, +with no extra external tooling, however you can replace any step of the build with any tool +you like. + +This will be a much more transparent build system which will allow for easier customisation +and address a lot of the issues that have been raised around it. + +## The Payoff + +These positive changes will be a huge benefit to the project: +- The new API will be much more intuitive and will allow for more complex applications + to be built. +- Using static analysis for bindings generation will be much faster and reduce a lot + of the complexity around the current process. +- Using an established, external build system will make the build process completely + transparent, allowing for powerful customisation. + +Benefits to the project maintainers are: + +- The new API will be much easier to maintain and adapt to new features and platforms. +- The new build system will be much easier to maintain and extend. I hope this will lead to a new ecosystem of community driven build pipelines. +- Better separation of concerns within the project. This will make it easier to add new features and platforms. + +## The Plan + +A lot of the experimentation for this has already been done and it's looking good. +There is no current timeline for this work but I'm hoping by the end of Q1 2023, there +will be an alpha release for Mac to allow the community to test, experiment with and +provide feedback. + +## Summary + +- The v2 API is declarative, hides a lot from the developer and not suitable for features such as multiple windows. A new API will be created which will be simpler, intuitive and more powerful. +- The build system is opaque and difficult to customise so we will move to an external build system which will open it all up. +- The bindings generation is slow and complex so we will move to static analysis which will remove a lot of the complexity the current method has. + +There has been a lot of work put into the guts of v2 and it's solid. +It's now time to address the layer on top of it and make it a much better experience for the developer. + +I hope you are as excited about this as I am. I'm looking forward to hearing your thoughts and feedback. + +Regards, + +‐ Lea + +PS: If you or your company find Wails useful, please consider [sponsoring the project](https://github.com/sponsors/leaanthony). Thanks! + +PPS: Yes, that's a genuine screenshot of a multi-window application built with Wails. It's not a mockup. It's real. It's awesome. It's coming soon. \ No newline at end of file diff --git a/website/blog/2025-03-16-security-incident-response.mdx b/website/blog/2025-03-16-security-incident-response.mdx new file mode 100644 index 000000000..e9903c570 --- /dev/null +++ b/website/blog/2025-03-16-security-incident-response.mdx @@ -0,0 +1,89 @@ +--- +slug: security-incident-response-march-2025 +title: Proactive Security Response - GitHub Actions Supply Chain Attack +authors: [leaanthony] +tags: [wails, security] +--- + +
+ Security Shield +
+
+ +:::note TL;DR +**Good news! Wails was NOT affected by this security incident.** Our thorough investigation confirmed that no secrets were leaked, and the Wails codebase and releases remain completely secure. We've already taken proactive measures to further strengthen our security posture. +::: + +## Introduction + +On 15th March 2025 (AEST), the Wails team was alerted to a security incident involving the `tj-actions/changed-files` GitHub Action. This widely-used action (with over 23,000 repositories depending on it) was compromised in a supply chain attack. While this action was used in some of our CI/CD workflows, we're pleased to confirm that Wails remained fully protected throughout. + +This post shares the details of the incident, our response, and the additional safeguards we've implemented to ensure the continued security of the Wails project. + +## Incident Details + +The security company StepSecurity [reported](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) that the `tj-actions/changed-files` GitHub Action was compromised beginning around 9:00 AM March 14th, 2025 Pacific Time (4:00 PM UTC). + +In this attack, malicious code was injected into the action that attempted to dump CI/CD secrets from GitHub Actions runner processes into public logs. The attackers modified the action's code and retroactively updated multiple version tags to reference the malicious commit. + +## Our Proactive Assessment + +Upon learning this, we immediately launched a comprehensive assessment of our systems: + +1. We identified the following Wails workflows that were using the action: + - For Wails v2: `pr-v2.yml` and `upload-source-documents.yml` + - For Wails v3: `pr-v3.yml`, `publish-npm.yml`, and `upload-source-documents.yml` + +2. Our security team conducted a thorough review of all workflow logs for the affected actions during the time period of the compromise. + +3. We're happy to confirm that **no secrets were leaked** in any of our workflow logs, and the Wails codebase remained completely secure. + +## Action Taken + +We took immediate steps to address this situation: + +1. We swiftly replaced all instances of the affected `tj-actions/changed-files` action with the secure alternative `step-security/changed-files` provided by StepSecurity. + +2. As an extra precautionary measure, we temporarily removed all secrets from our GitHub Actions workflows. + +## What This Means for You + +We want to reassure our community that: + +1. The Wails codebase was never compromised in any way. +2. No malicious code was introduced into any Wails releases. +3. This situation only potentially affected our CI/CD pipeline, not the actual Wails source code or releases. +4. No sensitive information or secrets were exposed during this time. + +**In short: All Wails releases remain secure and trustworthy, and no action is required on your part.** + +## Strengthening Our Security Posture + +To minimise exposure to similar potential incidents in the future, we're enhancing our security practices by: + +1. Implementing stricter version pinning for all third-party actions used in our workflows, preferably pinning to specific commit hashes rather than version tags. + +2. Establishing a regular security review process for our CI/CD pipelines and dependencies. + +3. Exploring the use of additional security tools like StepSecurity's Harden-Runner to provide enhanced protection for our GitHub Actions workflows. + +4. Developing a more comprehensive security incident response plan to ensure we can respond quickly and effectively to any future security concerns. + +It's worth noting that the Wails project already employs several security tools as part of our development process: + +- **Semgrep**: We use Semgrep for static code analysis to identify potential security vulnerabilities and code quality issues. +- **Snyk**: We employ Snyk to continuously monitor our dependencies for known vulnerabilities and receive alerts when security patches are needed. + +These existing security measures, combined with our enhanced preventative steps, demonstrate our ongoing commitment to maintaining the security and integrity of the Wails project. + +## Moving Forward + +The security of the Wails project and the trust of our community are our highest priorities. We remain committed to transparency and will continue to promptly address any security concerns that arise. + +We would like to thank StepSecurity for their quick response in identifying this issue and providing a secure alternative action. + +If you have any questions or concerns about this, please don't hesitate to reach out to us on [GitHub](https://github.com/wailsapp/wails) or [Discord](https://discord.gg/JDdSxwjhGf). We're always here to help. diff --git a/website/blog/authors.yml b/website/blog/authors.yml new file mode 100644 index 000000000..6212549b1 --- /dev/null +++ b/website/blog/authors.yml @@ -0,0 +1,11 @@ +leaanthony: + name: Lea Anthony + title: Maintainer of Wails + url: https://github.com/leaanthony + image_url: https://github.com/leaanthony.png + +misitebao: + name: Misite Bao + title: Architect + url: https://github.com/misitebao + image_url: https://github.com/misitebao.png diff --git a/website/bun.lockb b/website/bun.lockb new file mode 100644 index 000000000..63ed1b159 Binary files /dev/null and b/website/bun.lockb differ diff --git a/website/crowdin.yml b/website/crowdin.yml new file mode 100644 index 000000000..d04258c22 --- /dev/null +++ b/website/crowdin.yml @@ -0,0 +1,21 @@ +project_id: "531392" +api_token_env: CROWDIN_PERSONAL_TOKEN +preserve_hierarchy: true +commit_message: "[ci skip]" +files: + - source: /docs/**/* + translation: /i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name% + ignore: + - /**/*.json + - source: /blog/**/* + translation: /i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name% + - source: /src/pages/**/* + translation: /i18n/%two_letters_code%/docusaurus-plugin-content-pages/**/%original_file_name% + ignore: + - /**/*.js + - /**/*.jsx + - /**/*.ts + - /**/*.tsx + - /**/*.css + - source: /i18n/en/**/*.json + translation: /i18n/%two_letters_code%/**/%original_file_name% diff --git a/website/docs/appendix/_category_.json b/website/docs/appendix/_category_.json new file mode 100644 index 000000000..83af4ca28 --- /dev/null +++ b/website/docs/appendix/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Appendix", + "position": 70 +} diff --git a/website/docs/community/_category_.json b/website/docs/community/_category_.json new file mode 100644 index 000000000..524986e1e --- /dev/null +++ b/website/docs/community/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Community", + "position": 50 +} diff --git a/website/docs/community/links.mdx b/website/docs/community/links.mdx new file mode 100644 index 000000000..fe8b51dd0 --- /dev/null +++ b/website/docs/community/links.mdx @@ -0,0 +1,27 @@ +--- +sidebar_position: 2 +--- + +# Links + +This page serves as a list for community related links. Please submit a PR (click `Edit this page` at the bottom) +to submit links. + +## Awesome Wails + +The [definitive list](https://github.com/wailsapp/awesome-wails) of links related to Wails. + +## Support Channels + +- [Wails Discord Server](https://discord.gg/JDdSxwjhGf) +- [Github Issues](https://github.com/wailsapp/wails/issues) +- [v2 Beta Discussion Board](https://github.com/wailsapp/wails/discussions/828) + +## Social Media + +- [Twitter](https://twitter.com/wailsapp) +- [Wails Chinese Community QQ Group](https://qm.qq.com/cgi-bin/qm/qr?k=PmIURne5hFGNd7QWzW5qd6FV-INEjNJv&jump_from=webapi) - Group number: 1067173054 + +## Other Tutorials and Articles + +- [Building of Bulletin Board](https://blog.customct.com/building-bulletin-board) diff --git a/website/docs/community/showcase/_category_.json b/website/docs/community/showcase/_category_.json new file mode 100644 index 000000000..276e283b7 --- /dev/null +++ b/website/docs/community/showcase/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Showcase", + "position": 1 +} diff --git a/website/docs/community/showcase/bulletinboard.mdx b/website/docs/community/showcase/bulletinboard.mdx new file mode 100644 index 000000000..37be75135 --- /dev/null +++ b/website/docs/community/showcase/bulletinboard.mdx @@ -0,0 +1,10 @@ +# BulletinBoard + +```mdx-code-block +

+ +
+

+``` + +The [BulletinBoard](https://github.com/raguay/BulletinBoard) application is a versital message board for static messages or dialogs to get information from the user for a script. It has a TUI for creating new dialogs that can latter be used to get information from the user. It's design is to stay running on your system and show the information as needed and then hide away. I have a process for watching a file on my system and sending the contents to BulletinBoard when changed. It works great with my workflows. There is also an [Alfred workflow](https://github.com/raguay/MyAlfred/blob/master/Alfred%205/EmailIt.alfredworkflow) for sending information to the program. The workflow is also for working with [EmailIt](https://github.com/raguay/EmailIt). diff --git a/website/docs/community/showcase/cfntracker.mdx b/website/docs/community/showcase/cfntracker.mdx new file mode 100644 index 000000000..8fab23b75 --- /dev/null +++ b/website/docs/community/showcase/cfntracker.mdx @@ -0,0 +1,39 @@ +# CFN Tracker + +```mdx-code-block +

+ +
+

+``` + +[CFN Tracker](https://github.com/williamsjokvist/cfn-tracker) - Track any Street +Fighter 6 or V CFN profile's live matches. Check +[the website](https://cfn.williamsjokvist.se/) to get started. + +## Features + +- Real-time match tracking +- Storing match logs and statistics +- Support for displaying live stats to OBS via Browser Source +- Support for both SF6 and SFV +- Ability for users to create their own OBS Browser themes with CSS + +### Major tech used alongside Wails + +- [Task](https://github.com/go-task/task) - wrapping the Wails CLI to make + common commands easy to use +- [React](https://github.com/facebook/react) - chosen for its rich ecosystem + (radix, framer-motion) +- [Bun](https://github.com/oven-sh/bun) - used for its fast dependency + resolution and build-time +- [Rod](https://github.com/go-rod/rod) - headless browser automation for + authentication and polling changes +- [SQLite](https://github.com/mattn/go-sqlite3) - used for storing matches, + sessions and profiles +- [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) - + a http stream to send tracking updates to OBS browser sources +- [i18next](https://github.com/i18next/) - with backend connector to serve + localization objects from the Go layer +- [xstate](https://github.com/statelyai/xstate) - state machines for auth + process and tracking diff --git a/website/docs/community/showcase/clustta.mdx b/website/docs/community/showcase/clustta.mdx new file mode 100644 index 000000000..7da3195df --- /dev/null +++ b/website/docs/community/showcase/clustta.mdx @@ -0,0 +1,27 @@ +--- +title: Clustta +description: File manager and project management tool for creative professionals. +slug: /community/showcase/clustta +image: /img/showcase/clustta.png +--- + +
+ Clustta screenshot +
+ +[Clustta](https://clustta.com) is a file manager and project management tool designed for creative professionals. Built with Wails, it simplifies file management, collaboration, and version control for creative workflows. + +## Features + +- **File Management**: Track all projects and files with easy access even months after completion. +- **Version Control**: Save unlimited revisions with descriptive notes, without duplicating files. +- **Collaboration**: Share files or entire projects securely through simple user tags with fine-grained permissions. +- **Recovery**: Restore corrupted files from saved checkpoints if your software crashes. +- **Templates**: Quick start with preset project and task templates. +- **Kanban Boards**: Visual task tracking to keep tasks organized. +- **Dependencies**: Track and version project resources and dependencies. +- **Checkpoints**: Create memorable milestones and explore alternate creative directions non-destructively. +- **Search & Filters**: Powerful instant search with metadata filtering (task types, tags, status, file extensions). +- **Workspaces**: Save search and filter combinations for easy access to specific task sets. +- **Integrations**: Connect with creative software packages and production management tools like Blender and Kitsu. +- **Self-Hosting**: Host private instances for teams or studios on your own servers. diff --git a/website/docs/community/showcase/emailit.mdx b/website/docs/community/showcase/emailit.mdx new file mode 100644 index 000000000..c1817b70f --- /dev/null +++ b/website/docs/community/showcase/emailit.mdx @@ -0,0 +1,10 @@ +# EmailIt + +```mdx-code-block +

+ +
+

+``` + +[EmailIt](https://github.com/raguay/EmailIt/) is a Wails 2 program that is a markdown based email sender only with nine notepads, scripts to manipulate the text, and templates. It also has a scripts terminal to run scripts in EmailIt on files in your system. The scripts and templates can be used from the commandline itself or with the Alfred, Keyboard Maestro, Dropzone, or PopClip extensions. It also supports scripts and themes downloaded form GitHub. Documentation is not complete, but the programs works. It’s built using Wails2 and Svelte, and the download is a universal macOS application. diff --git a/website/docs/community/showcase/encrypteasy.mdx b/website/docs/community/showcase/encrypteasy.mdx new file mode 100644 index 000000000..7504950ea --- /dev/null +++ b/website/docs/community/showcase/encrypteasy.mdx @@ -0,0 +1,12 @@ +# EncryptEasy + +```mdx-code-block +

+ +
+

+``` + +**[EncryptEasy](https://www.encrypteasy.app) is a simple and easy to use PGP encryption tool, managing all your and your contacts keys. Encryption should be simple. Developed with Wails.** + +Encrypting messages using PGP is the industry standard. Everyone has a private and a public key. Your private key, well, needs to be kept private so only you can read messages. Your public key is distributed to anyone who wants to send you secret, encrypted messages. Managing keys, encrypting messages and decrypting messages should be a smooth experience. EncryptEasy is all about making it easy. diff --git a/website/docs/community/showcase/espstudio.mdx b/website/docs/community/showcase/espstudio.mdx new file mode 100644 index 000000000..44db858f9 --- /dev/null +++ b/website/docs/community/showcase/espstudio.mdx @@ -0,0 +1,13 @@ +# ESP Studio + +```mdx-code-block +

+ +
+

+``` + +[ESP Studio](https://github.com/torabian/esp-studio) - Cross platform, Desktop, Cloud, and Embedded software +for controlling ESP/Arduino devices, and building complex IOT workflows and control systems diff --git a/website/docs/community/showcase/filehound.mdx b/website/docs/community/showcase/filehound.mdx new file mode 100644 index 000000000..8c541f482 --- /dev/null +++ b/website/docs/community/showcase/filehound.mdx @@ -0,0 +1,24 @@ +# FileHound Export Utility + +```mdx-code-block +

+ +
+

+``` + +[FileHound Export Utility](https://www.filehound.co.uk/) FileHound is a cloud document management platform made for secure file retention, business process automation and SmartCapture capabilities. + +The FileHound Export Utility allows FileHound Administrators the ability to run a secure document and data extraction tasks for alternative back-up and recovery purposes. This application will download all documents and/or meta data saved in FileHound based on the filters you choose. The metadata will be exported in both JSON and XML formats. + +Backend built with: +Go 1.15 +Wails 1.11.0 +go-sqlite3 1.14.6 +go-linq 3.2 + +Frontend with: +Vue 2.6.11 +Vuex 3.4.0 +TypeScript +Tailwind 1.9.6 diff --git a/website/docs/community/showcase/gamestacker.mdx b/website/docs/community/showcase/gamestacker.mdx new file mode 100644 index 000000000..46245e146 --- /dev/null +++ b/website/docs/community/showcase/gamestacker.mdx @@ -0,0 +1,19 @@ +--- +title: GameStacker +description: A modern, console-like interface that unifies your game libraries. +slug: /community/showcase/gamestacker +image: /img/showcase/gamestacker.webp +--- + +
+ GameStacker main dashboard +
+ +[GameStacker](https://gamestacker.io) is a modern, elegant console-like interface that unifies your entire game collection into one beautiful dashboard. + +## Features + +- **Unified Library**: Automatically detects and imports games from external libraries (such as Steam, LaunchBox, and RetroBat), bringing your modern and retro collections into a single, cohesive interface. +- **True Console Experience**: Built for controllers with responsive navigation, UI sound effects, and a dedicated in-game "Guide" overlay to control music and manage your session without alt-tabbing. +- **Achievement Integration**: Tracks your progress across supported services, allowing you to view unlocks and Gamerscore directly from the dashboard. +- **Multi-Profile Support**: Create unique profiles with custom avatars and specific color themes for a personalized experience. diff --git a/website/docs/community/showcase/grpcmd-gui.mdx b/website/docs/community/showcase/grpcmd-gui.mdx new file mode 100644 index 000000000..891350290 --- /dev/null +++ b/website/docs/community/showcase/grpcmd-gui.mdx @@ -0,0 +1,10 @@ +# grpcmd-gui + +```mdx-code-block +

+ +
+

+``` + +[grpcmd-gui](https://grpc.md/gui) is a modern cross-platform desktop app and API client for gRPC development and testing. diff --git a/website/docs/community/showcase/hiposter.mdx b/website/docs/community/showcase/hiposter.mdx new file mode 100644 index 000000000..c0f9052c3 --- /dev/null +++ b/website/docs/community/showcase/hiposter.mdx @@ -0,0 +1,10 @@ +# hiposter + +```mdx-code-block +

+ +
+

+``` + +[hiposter](https://github.com/obity/hiposter) is a simple and efficient http API testing client tool. Based on Wails, Go and sveltejs. \ No newline at end of file diff --git a/website/docs/community/showcase/kafka-king.mdx b/website/docs/community/showcase/kafka-king.mdx new file mode 100644 index 000000000..0ba78a6ad --- /dev/null +++ b/website/docs/community/showcase/kafka-king.mdx @@ -0,0 +1,22 @@ +# Kafka-King + +```mdx-code-block +

+ +
+

+``` + +[Kafka-King](https://github.com/Bronya0/Kafka-King) is a kafka GUI client that supports various systems and is compact and easy to use. +This is made of Wails+vue3 + +# Kafka-King function list +- [x] View the cluster node list, support dynamic configuration of broker and topic configuration items +- [x] Supports consumer clients, consumes the specified topic, size, and timeout according to the specified group, and displays the message information in various dimensions in a table +- [x] Supports PLAIN, SSL, SASL, kerberos, sasl_plaintext, etc. etc. +- [x] Create topics (support batches), delete topics, specify replicas, partitions +- [x] Support statistics of the total number of messages, total number of submissions, and backlog for each topic based on consumer groups +- [x] Support viewing topics Detailed information (offset) of the partition, and support adding additional partitions +- [x] Support simulated producers, batch sending messages, specify headers, partitions +- [x] Health check +- [x] Support viewing consumer groups , Consumer- …… diff --git a/website/docs/community/showcase/marasi.mdx b/website/docs/community/showcase/marasi.mdx new file mode 100644 index 000000000..5ff137523 --- /dev/null +++ b/website/docs/community/showcase/marasi.mdx @@ -0,0 +1,22 @@ +# Marasi + +```mdx-code-block +

+ +
+

+``` + +[Marasi](https://marasi.app/) is an open source application security testing proxy, it lets you intercept, inspect, modify, and extend requests as they flow through your applications. Read more about it on the [blog](https://marasi.app/blog/2025/introducing_marasi/). + +## Features + +- Desktop GUI Interface: Cross-platform desktop application built with Wails +- HTTP/HTTPS Proxy: TLS-capable proxy server with certificate management +- Request/Response Interception: Modify traffic in real-time with an intuitive interface +- Lua Extensions: Scriptable proxy behavior with built-in extensions +- Project Management: SQLite-based storage for all proxy data (requests, responses, metadata) +- Launchpad: Replay and modify HTTP requests +- Scope Management: Filter traffic with inclusion/exclusion rules +- Waypoints: Override hostnames for request routing +- Chrome Integration: Auto-configure Chrome with proxy settings \ No newline at end of file diff --git a/website/docs/community/showcase/mchat.mdx b/website/docs/community/showcase/mchat.mdx new file mode 100644 index 000000000..aa535a6f8 --- /dev/null +++ b/website/docs/community/showcase/mchat.mdx @@ -0,0 +1,10 @@ +# Mchat + +```mdx-code-block +

+ +
+

+``` + +[Official page](https://marcio199226.github.io/mchat-site/public/) Fully anonymous end2end encrypted chat. diff --git a/website/docs/community/showcase/minecraftupdater.mdx b/website/docs/community/showcase/minecraftupdater.mdx new file mode 100644 index 000000000..2f6c7c72b --- /dev/null +++ b/website/docs/community/showcase/minecraftupdater.mdx @@ -0,0 +1,14 @@ +# Minecraft Updater + +```mdx-code-block +

+ +
+

+``` + +[Minecraft Updater](https://github.com/Gurkengewuerz/MinecraftModUpdater) is a utility tool to update and synchronize Minecraft mods for your userbase. It’s built using Wails2 and React with [antd](https://ant.design/) as frontend framework. diff --git a/website/docs/community/showcase/minesweeper-xp.mdx b/website/docs/community/showcase/minesweeper-xp.mdx new file mode 100644 index 000000000..f127a005f --- /dev/null +++ b/website/docs/community/showcase/minesweeper-xp.mdx @@ -0,0 +1,10 @@ +# Minesweeper XP + +```mdx-code-block +

+ +
+

+``` + +[Minesweeper-XP](https://git.new/Minesweeper-XP) allows you to experience the classic Minesweeper XP (+ 98 and 3.1) on macOS, Windows, and Linux! diff --git a/website/docs/community/showcase/modalfilemanager.mdx b/website/docs/community/showcase/modalfilemanager.mdx new file mode 100644 index 000000000..bcd212396 --- /dev/null +++ b/website/docs/community/showcase/modalfilemanager.mdx @@ -0,0 +1,14 @@ +# Modal File Manager + +```mdx-code-block +

+ +
+

+``` + +[Modal File Manager](https://github.com/raguay/ModalFileManager) is a dual pane file manager using web technologies. My original design was based on NW.js and can be found [here](https://github.com/raguay/ModalFileManager-NWjs). This version uses the same Svelte based frontend code (but it has be greatly modified since the departure from NW.js), but the backend is a [Wails 2](https://wails.io/) implementation. By using this implementation, I no longer use command line `rm`, `cp`, etc. commands, but a git install has to be on the system to download themes and extensions. It is fully coded using Go and runs much faster than the previous versions. + +This file manager is designed around the same principle as Vim: a state controlled keyboard actions. The number of states isn't fixed, but very programmable. Therefore, an infinite number of keyboard configurations can be created and used. This is the main difference from other file managers. There are themes and extensions available to download from GitHub. diff --git a/website/docs/community/showcase/mollywallet.mdx b/website/docs/community/showcase/mollywallet.mdx new file mode 100644 index 000000000..5d846d06d --- /dev/null +++ b/website/docs/community/showcase/mollywallet.mdx @@ -0,0 +1,10 @@ +# Molley Wallet + +```mdx-code-block +

+ +
+

+``` + +[Molly Wallet](https://github.com/grvlle/constellation_wallet/) the official $DAG wallet of the Constellation Network. It'll let users interact with the Hypergraph Network in various ways, not limited to producing $DAG transactions. diff --git a/website/docs/community/showcase/october.mdx b/website/docs/community/showcase/october.mdx new file mode 100644 index 000000000..66d634dc5 --- /dev/null +++ b/website/docs/community/showcase/october.mdx @@ -0,0 +1,14 @@ +# October + +```mdx-code-block +

+ +
+

+``` + +[October](https://october.utf9k.net) is a small Wails application that makes it really easy to extract highlights from [Kobo eReaders](https://en.wikipedia.org/wiki/Kobo_eReader) and then forward them to [Readwise](https://readwise.io). + +It has a relatively small scope with all platform versions weighing in under 10MB, and that's without enabling [UPX compression](https://upx.github.io/)! + +In contrast, the author's previous attempts with Electron quickly bloated to several hundred megabytes. diff --git a/website/docs/community/showcase/optimus.mdx b/website/docs/community/showcase/optimus.mdx new file mode 100644 index 000000000..4f87479d6 --- /dev/null +++ b/website/docs/community/showcase/optimus.mdx @@ -0,0 +1,10 @@ +# Optimus + +```mdx-code-block +

+ +
+

+``` + +[Optimus](https://github.com/splode/optimus) is a desktop image optimization application. It supports conversion and compression between WebP, JPEG, and PNG image formats. diff --git a/website/docs/community/showcase/portfall.mdx b/website/docs/community/showcase/portfall.mdx new file mode 100644 index 000000000..03e740f4c --- /dev/null +++ b/website/docs/community/showcase/portfall.mdx @@ -0,0 +1,10 @@ +# Portfall + +```mdx-code-block +

+ +
+

+``` + +[Portfall](https://github.com/rekon-oss/portfall) - A desktop k8s port-forwarding portal for easy access to all your cluster UIs diff --git a/website/docs/community/showcase/resizem.mdx b/website/docs/community/showcase/resizem.mdx new file mode 100644 index 000000000..27f168f48 --- /dev/null +++ b/website/docs/community/showcase/resizem.mdx @@ -0,0 +1,10 @@ +# Resizem + +```mdx-code-block +

+ +
+

+``` + +[Resizem](https://github.com/barats/resizem) - is an app designed for bulk image process. It is particularly useful for users who need to resize, convert, and manage large numbers of image files at once. diff --git a/website/docs/community/showcase/restic-browser.mdx b/website/docs/community/showcase/restic-browser.mdx new file mode 100644 index 000000000..3646384ec --- /dev/null +++ b/website/docs/community/showcase/restic-browser.mdx @@ -0,0 +1,12 @@ +# Restic Browser + +```mdx-code-block +

+ +
+

+``` + +[Restic-Browser](https://github.com/emuell/restic-browser) - A simple, cross-platform [restic](https://github.com/restic/restic) backup GUI for browsing and restoring restic repositories. diff --git a/website/docs/community/showcase/riftshare.mdx b/website/docs/community/showcase/riftshare.mdx new file mode 100644 index 000000000..9928b4785 --- /dev/null +++ b/website/docs/community/showcase/riftshare.mdx @@ -0,0 +1,21 @@ +# RiftShare + +```mdx-code-block +

+ +
+

+``` + +Easy, Secure, and Free file sharing for everyone. Learn more at [Riftshare.app](https://riftshare.app) + +## Features + +- Easy secure file sharing between computers both in the local network and through the internet +- Supports sending files or directories securely through the [magic wormhole protocol](https://magic-wormhole.readthedocs.io/en/latest/) +- Compatible with all other apps using magic wormhole (magic-wormhole or wormhole-william CLI, wormhole-gui, etc.) +- Automatic zipping of multiple selected files to send at once +- Full animations, progress bar, and cancellation support for sending and receiving +- Native OS File Selection +- Open files in one click once received +- Auto Update - don't worry about having the latest release! diff --git a/website/docs/community/showcase/scriptbar.mdx b/website/docs/community/showcase/scriptbar.mdx new file mode 100644 index 000000000..3e41eb32a --- /dev/null +++ b/website/docs/community/showcase/scriptbar.mdx @@ -0,0 +1,10 @@ +# ScriptBar + +```mdx-code-block +

+ +
+

+``` + +[ScriptBar](https://GitHub.com/raguay/ScriptBarApp) is a program to show the output of scripts or [Node-Red](https://nodered.org) server. It runs scripts defined in EmailIt program and shows the output. Scripts from xBar or TextBar can be used, but currently on the TextBar scripts work well. It also displays the output of scripts on your system. ScriptBar doesn't put them in the menubar, but has them all in a convient window for easy viewing. You can have multiple tabs to have many different things show. You can also keep the links to your most visited web sites. diff --git a/website/docs/community/showcase/snippetexpander.mdx b/website/docs/community/showcase/snippetexpander.mdx new file mode 100644 index 000000000..1f9fb6157 --- /dev/null +++ b/website/docs/community/showcase/snippetexpander.mdx @@ -0,0 +1,27 @@ +# Snippet Expander + +```mdx-code-block +

+ +
+ Screenshot of Snippet Expander's Select Snippet window +

+

+ +
+ Screenshot of Snippet Expander's Add Snippet screen +

+

+ +
+ Screenshot of Snippet Expander's Search & Paste window +

+``` + +[Snippet Expander](https://snippetexpander.org) is "Your little expandable text snippets helper", for Linux. + +Snippet Expander comprises of a GUI application built with Wails for managing snippets and settings, with a Search & Paste window mode for quickly selecting and pasting a snippet. + +The Wails based GUI, go-lang CLI and vala-lang auto expander daemon all communicate with a go-lang daemon via D-Bus. The daemon does the majority of the work, managing the database of snippets and common settings, and providing services for expanding and pasting snippets etc. + +Check out the [source code](https://git.sr.ht/~ianmjones/snippetexpander/tree/trunk/item/cmd/snippetexpandergui/app.go#L38) to see how the Wails app sends messages from the UI to the backend that are then sent to the daemon, and subscribes to a D-Bus event to monitor changes to snippets via another instance of the app or CLI and show them instantly in the UI via a Wails event. diff --git a/website/docs/community/showcase/surge.mdx b/website/docs/community/showcase/surge.mdx new file mode 100644 index 000000000..c3b3fb4c0 --- /dev/null +++ b/website/docs/community/showcase/surge.mdx @@ -0,0 +1,10 @@ +# Surge + +```mdx-code-block +

+ +
+

+``` + +[Surge](https://getsurge.io/) is a p2p filesharing app designed to utilize blockchain technologies to enable 100% anonymous file transfers. Surge is end-to-end encrypted, decentralized and open source. diff --git a/website/docs/community/showcase/tinyrdm.mdx b/website/docs/community/showcase/tinyrdm.mdx new file mode 100644 index 000000000..e3124bab7 --- /dev/null +++ b/website/docs/community/showcase/tinyrdm.mdx @@ -0,0 +1,11 @@ +# Tiny RDM + +```mdx-code-block +

+ + +
+

+``` + +The [Tiny RDM](https://redis.tinycraft.cc/) application is an open-source, modern lightweight Redis GUI. It has a beautful UI, intuitive Redis database management, and compatible with Windows, Mac, and Linux. It provides visual key-value data operations, supports various data decoding and viewing options, built-in console for executing commands, slow log queries and more. diff --git a/website/docs/community/showcase/upbeat.mdx b/website/docs/community/showcase/upbeat.mdx new file mode 100644 index 000000000..2f85b6cce --- /dev/null +++ b/website/docs/community/showcase/upbeat.mdx @@ -0,0 +1,18 @@ +--- +title: UpBeat +description: An RSS/Atom Feed Reader that filters out negative news with locally run ML models. +slug: /community/showcase/upbeat +image: /img/showcase/upbeat.png +--- + +
+ UpBeat screenshot +
+ +[UpBeat](https://upbeat.mitchelltechnologies.co.uk) is An RSS/Atom Feed Reader for macOS that filters out negative news with locally running ML models. + +## Features + +- **Local ML**: UpBeat runs a transformer-based sentiment analysis model directly on your Mac's hardware. No waiting for a Cloud GPU to spin up before you can decide what you want to read +- **Apple Neural Engine Enabled**: UpBeat runs its ML models directly on the Neural Engine of Macs. That means you get super-fast inference, but without burning through your battery or electricity bill. +- **Widely Compatible**: Works with the vast majority of RSS + Atom versions. diff --git a/website/docs/community/showcase/wailsterm.mdx b/website/docs/community/showcase/wailsterm.mdx new file mode 100644 index 000000000..9924dace5 --- /dev/null +++ b/website/docs/community/showcase/wailsterm.mdx @@ -0,0 +1,10 @@ +# WailsTerm + +```mdx-code-block +

+ +
+

+``` + +[WailsTerm](https://github.com/rlshukhov/wailsterm) is a simple translucent terminal app powered by Wails and Xterm.js. diff --git a/website/docs/community/showcase/wally.mdx b/website/docs/community/showcase/wally.mdx new file mode 100644 index 000000000..7408aa585 --- /dev/null +++ b/website/docs/community/showcase/wally.mdx @@ -0,0 +1,10 @@ +# Wally + +```mdx-code-block +

+ +
+

+``` + +[Wally](https://ergodox-ez.com/pages/wally) is the official firmware flasher for [Ergodox](https://ergodox-ez.com/) keyboards. It looks great and is a fantastic example of what you can achieve with Wails: the ability to combine the power of Go and the rich graphical tools of the web development world. diff --git a/website/docs/community/showcase/warmine.mdx b/website/docs/community/showcase/warmine.mdx new file mode 100644 index 000000000..46b10b5b1 --- /dev/null +++ b/website/docs/community/showcase/warmine.mdx @@ -0,0 +1,19 @@ +# Minecraft launcher for WarMine + +```mdx-code-block +

+ + +
+

+``` + +[Minecraft launcher for WarMine](https://warmine.ru/) is a Wails application, that allows you to easily join modded game servers and manage your game accounts. + +The Launcher downloads the game files, checks their integrity and launches the game with a wide range of customization options for the launch arguments from the backend. + +Frontend is written in Svelte, whole launcher fits in 9MB and supports Windows 7-11. diff --git a/website/docs/community/showcase/wombat.mdx b/website/docs/community/showcase/wombat.mdx new file mode 100644 index 000000000..f100c55e2 --- /dev/null +++ b/website/docs/community/showcase/wombat.mdx @@ -0,0 +1,10 @@ +# Wombat + +```mdx-code-block +

+ +
+

+``` + +[Wombat](https://github.com/rogchap/wombat) is a cross platform gRPC client. diff --git a/website/docs/community/showcase/ytd.mdx b/website/docs/community/showcase/ytd.mdx new file mode 100644 index 000000000..5db428f72 --- /dev/null +++ b/website/docs/community/showcase/ytd.mdx @@ -0,0 +1,10 @@ +# Ytd + +```mdx-code-block +

+ +
+

+``` + +[Ytd](https://github.com/marcio199226/ytd/tree/v2-wails) is an app for downloading tracks from youtube, creating offline playlists and share them with your friends, your friends will be able to playback your playlists or download them for offline listening, has an built-in player. diff --git a/website/docs/community/templates.mdx b/website/docs/community/templates.mdx new file mode 100644 index 000000000..3b020b60b --- /dev/null +++ b/website/docs/community/templates.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 1 +--- + +# Templates + +This page serves as a list for community supported templates. Please submit a PR (click `Edit this page` at the bottom) +to include your templates. To build your own template, please see the [Templates](../guides/templates.mdx) guide. + +To use these templates, run `wails init -n "Your Project Name" -t [the link below[@version]]` + +If there is no version suffix, the main branch code template is used by default. If there is a version suffix, the code template corresponding to the tag of this version is used. + +Example: `wails init -n "Your Project Name" -t https://github.com/misitebao/wails-template-vue` + +:::warning Attention + +**The Wails project does not maintain, is not responsible nor liable for 3rd party templates!** + +If you are unsure about a template, inspect `package.json` and `wails.json` for what scripts are run and what packages are installed. + +::: + +## Vue + +- [wails-template-vue](https://github.com/misitebao/wails-template-vue) - Wails template based on Vue ecology (Integrated TypeScript, Dark theme, Internationalization, Single page routing, TailwindCSS) +- [wails-template-quasar-js](https://github.com/sgosiaco/wails-template-quasar-js) - A template using JavaScript + Quasar V2 (Vue 3, Vite, Sass, Pinia, ESLint, Prettier) +- [wails-template-quasar-ts](https://github.com/sgosiaco/wails-template-quasar-ts) - A template using TypeScript + Quasar V2 (Vue 3, Vite, Sass, Pinia, ESLint, Prettier, Composition API with <script setup>) +- [wails-template-naive](https://github.com/tk103331/wails-template-naive) - Wails template based on Naive UI (A Vue 3 Component Library) +- [wails-template-primevue-sakai](https://github.com/TekWizely/wails-template-primevue-sakai) - Wails starter using [PrimeVue's Sakai Application Template](https://sakai.primevue.org) (Vite, Vue, PrimeVue, TailwindCSS, Vue Router, Themes, Dark Mode, UI Components, and more) +- [wails-template-tdesign-js](https://github.com/tongque0/wails-template-tdesign-js) - Wails template based on TDesign UI (a Vue 3 UI library by Tencent), using Vite, Pinia, Vue Router, ESLint, and Prettier. + +## Angular + +- [wails-template-angular](https://github.com/mateothegreat/wails-template-angular) - Angular 15+ action packed & ready to roll to production. +- [wails-angular-template](https://github.com/TAINCER/wails-angular-template) - Angular with TypeScript, Sass, Hot-Reload, Code-Splitting and i18n + +## React + +- [wails-react-template](https://github.com/AlienRecall/wails-react-template) - A template using reactjs +- [wails-react-template](https://github.com/flin7/wails-react-template) - A minimal template for React that supports live development +- [wails-template-nextjs](https://github.com/LGiki/wails-template-nextjs) - A template using Next.js and TypeScript +- [wails-template-nextjs-app-router](https://github.com/thisisvk-in/wails-template-nextjs-app-router) - A template using Next.js and TypeScript with App router +- [wails-vite-react-ts-tailwind-template](https://github.com/hotafrika/wails-vite-react-ts-tailwind-template) - A template for React + TypeScript + Vite + TailwindCSS +- [Wails-vite-ts-tailwindcss-shadcn-template-2025](https://github.com/darkb0ts/Wails-vite-ts-tailwindcss-shadcn-template-2025) - A template for React + TypeScript + Vite +- [wails-vite-react-ts-tailwind-shadcnui-template](https://github.com/Mahcks/wails-vite-react-tailwind-shadcnui-ts) - A template with Vite, React, TypeScript, TailwindCSS, and shadcn/ui +- [wails-nextjs-tailwind-template](https://github.com/kairo913/wails-nextjs-tailwind-template) - A template using Next.js and Typescript with TailwindCSS + +## Svelte + +- [wails-svelte-template](https://github.com/raitonoberu/wails-svelte-template) - A template using Svelte +- [wails-vite-svelte-template](https://github.com/BillBuilt/wails-vite-svelte-template) - A template using Svelte and Vite +- [wails-vite-svelte-ts-tailwind-template](https://github.com/xvertile/wails-vite-svelte-tailwind-template) - A template using Wails, Svelte, Vite, TypeScript, and TailwindCSS v3 +- [wails-vite-svelte-tailwind-template](https://github.com/BillBuilt/wails-vite-svelte-tailwind-template) - A template using Svelte and Vite with TailwindCSS v3 +- [wails-svelte-tailwind-vite-template](https://github.com/PylotLight/wails-vite-svelte-tailwind-template/tree/master) - An updated template using Svelte v4.2.0 and Vite with TailwindCSS v3.3.3 +- [wails-sveltekit-template](https://github.com/h8gi/wails-sveltekit-template) - A template using SvelteKit +- [wails-template-sveltekit-less-prettier-eslint](https://github.com/Alex6357/wails-template-sveltekit-less-prettier-eslint) - A template using SvelteKit with less, Prettier and ESlint +- [wails-template-svelte-ts-less-prettier-eslint-vite](https://github.com/Alex6357/wails-template-svelte-ts-less-prettier-eslint-vite) - A template using Svelte5 + TypeScript + less + Prettier + ESlint + Vite + +## Solid + +- [wails-template-vite-solid-ts](https://github.com/xijaja/wails-template-solid-ts) - A template using Solid + Ts + Vite +- [wails-template-vite-solid-js](https://github.com/xijaja/wails-template-solid-js) - A template using Solid + Js + Vite + +## Elm + +- [wails-elm-template](https://github.com/benjamin-thomas/wails-elm-template) - Develop your GUI app with functional programming and a **snappy** hot-reload setup :tada: :rocket: +- [wails-template-elm-tailwind](https://github.com/rnice01/wails-template-elm-tailwind) - Combine the powers :muscle: of Elm + Tailwind CSS + Wails! Hot reloading supported. + +## HTMX + +- [wails-htmx-tailwind-daisyui-template](https://github.com/ltcovalt/wails-htmx-tailwind-daisyui-template) - HTMX template using Tailwind CSS + daisyUI for styling and the Go standard library for routing and HTML templating +- [wails-htmx-templ-chi-tailwind](https://github.com/PylotLight/wails-hmtx-templ-template) - Use a unique combination of pure htmx for interactivity plus templ for creating components and forms + +## Pure JavaScript (Vanilla) + +- [wails-pure-js-template](https://github.com/KiddoV/wails-pure-js-template) - A template with nothing but just basic JavaScript, HTML, and CSS + + +## Lit (web components) + +- [wails-lit-shoelace-esbuild-template](https://github.com/Braincompiler/wails-lit-shoelace-esbuild-template) - Wails template providing frontend with lit, Shoelace component library + pre-configured prettier and typescript. diff --git a/website/docs/gettingstarted/_category_.json b/website/docs/gettingstarted/_category_.json new file mode 100644 index 000000000..597b920df --- /dev/null +++ b/website/docs/gettingstarted/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Getting Started", + "position": 10 +} diff --git a/website/docs/gettingstarted/building.mdx b/website/docs/gettingstarted/building.mdx new file mode 100644 index 000000000..c4c16ea48 --- /dev/null +++ b/website/docs/gettingstarted/building.mdx @@ -0,0 +1,27 @@ +--- +sidebar_position: 6 +--- + +# Compiling your Project + +From the project directory, run `wails build`. +This will compile your project and save the production-ready binary in the `build/bin` directory. + +:::info Linux +If you are using a Linux distribution that does not have webkit2gtk-4.0 (such as Ubuntu 24.04), you will need to add `-tags webkit2_41`. +::: + +If you run the binary, you should see the default application: + +```mdx-code-block +
+ +
+
+``` + +For more details on compilation options, please refer to the [CLI Reference](../reference/cli.mdx#build). diff --git a/website/docs/gettingstarted/development.mdx b/website/docs/gettingstarted/development.mdx new file mode 100644 index 000000000..034be31d6 --- /dev/null +++ b/website/docs/gettingstarted/development.mdx @@ -0,0 +1,16 @@ +--- +sidebar_position: 5 +--- + +# Developing your Application + +You can run your application in development mode by running `wails dev` from your project directory. This will do the following things: + +- Build your application and run it +- Bind your Go code to the frontend so it can be called from JavaScript +- Using the power of [Vite](https://vitejs.dev/), will watch for modifications in your Go files and rebuild/re-run on change +- Sets up a [webserver](http://localhost:34115) that will serve your application over a browser. This allows you to use your favourite browser extensions. You can even call your Go code from the console + +To get started, run `wails dev` in the project directory. More information on this can be found [here](../reference/cli.mdx#dev). + +Coming soon: Tutorial diff --git a/website/docs/gettingstarted/firstproject.mdx b/website/docs/gettingstarted/firstproject.mdx new file mode 100644 index 000000000..5cf4dff58 --- /dev/null +++ b/website/docs/gettingstarted/firstproject.mdx @@ -0,0 +1,130 @@ +--- +sidebar_position: 2 +--- + +# Creating a Project + +## Project Generation + +Now that the CLI is installed, you can generate a new project by using the `wails init` command. + +Pick your favourite framework: + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + + + + Generate a Svelte project using JavaScript with:

+ + wails init -n myproject -t svelte + +If you would rather use TypeScript:
+ + wails init -n myproject -t svelte-ts + +
+ + Generate a React project using JavaScript with:

+ + wails init -n myproject -t react + +If you would rather use TypeScript:
+ + wails init -n myproject -t react-ts + +
+ + Generate a Vue project using JavaScript with:

+ + wails init -n myproject -t vue + +If you would rather use TypeScript:
+ + wails init -n myproject -t vue-ts + +
+ + Generate a Preact project using JavaScript with:

+ + wails init -n myproject -t preact + +If you would rather use TypeScript:
+ + wails init -n myproject -t preact-ts + +
+ + Generate a Lit project using JavaScript with:

+ + wails init -n myproject -t lit + +If you would rather use TypeScript:
+ + wails init -n myproject -t lit-ts + +
+ + Generate a Vanilla project using JavaScript with:

+ + wails init -n myproject -t vanilla + +If you would rather use TypeScript:
+ + wails init -n myproject -t vanilla-ts + +
+
+``` + +
+ +There are also [community templates](../community/templates.mdx) available that offer different capabilities and frameworks. + +To see the other options available, you can run `wails init -help`. +More details can be found in the [CLI Reference](../reference/cli.mdx#init). + +## Project Layout + +Wails projects have the following layout: + +``` +. +├── build/ +│ ├── appicon.png +│ ├── darwin/ +│ └── windows/ +├── frontend/ +├── go.mod +├── go.sum +├── main.go +└── wails.json +``` + +### Project structure rundown + +- `/main.go` - The main application +- `/frontend/` - Frontend project files +- `/build/` - Project build directory +- `/build/appicon.png` - The application icon +- `/build/darwin/` - Mac specific project files +- `/build/windows/` - Windows specific project files +- `/wails.json` - The project configuration +- `/go.mod` - Go module file +- `/go.sum` - Go module checksum file + +The `frontend` directory has nothing specific to Wails and can be any frontend project of your choosing. + +The `build` directory is used during the build process. These files may be updated to customise your builds. If +files are removed from the build directory, default versions will be regenerated. diff --git a/website/docs/gettingstarted/installation.mdx b/website/docs/gettingstarted/installation.mdx new file mode 100644 index 000000000..6189c6d83 --- /dev/null +++ b/website/docs/gettingstarted/installation.mdx @@ -0,0 +1,103 @@ +--- +sidebar_position: 1 +--- + +# Installation + +## Supported Platforms + +- Windows 10/11 AMD64/ARM64 +- MacOS 10.15+ AMD64 for development, MacOS 10.13+ for release +- MacOS 11.0+ ARM64 +- Linux AMD64/ARM64 + +## Dependencies + +Wails has a number of common dependencies that are required before installation: + +- Go 1.21+ (macOS 15+ requires Go 1.23.3+) +- NPM (Node 15+) + +### Go + +Download Go from the [Go Downloads Page](https://go.dev/dl/). + +Ensure that you follow the official [Go installation instructions](https://go.dev/doc/install). You will also need to ensure that your `PATH` environment variable also includes the path to your `~/go/bin` directory. Restart your terminal and do the following checks: + +- Check Go is installed correctly: `go version` +- Check "~/go/bin" is in your PATH variable: `echo $PATH | grep go/bin` + +### NPM + +Download NPM from the [Node Downloads Page](https://nodejs.org/en/download/). It is best to use the latest release as that is what we generally test against. + +Run `npm --version` to verify. + +## Platform Specific Dependencies + +You will also need to install platform specific dependencies: + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + + + + Wails requires that the xcode command line tools are installed. This can be + done by running xcode-select --install. + + + Wails requires that the WebView2 runtime is installed. Some Windows installations will already have this installed. You can check using the wails doctor command. + + + Linux requires the standard gcc build tools plus libgtk3 and libwebkit. Rather than list a ton of commands for different distros, Wails can try to determine what the installation commands are for your specific distribution. Run wails doctor after installation to be shown how to install the dependencies. If your distro/package manager is not supported, please consult the Add Linux Distro guide. +
Note:
+ If you are using latest Linux version (example: Ubuntu 24.04) and it is not supporting libwebkit2gtk-4.0-dev, then you might encounter an issue in wails doctor: libwebkit not found. To resolve this issue you can install libwebkit2gtk-4.1-dev and during your build use the tag -tags webkit2_41. +

+ After installing Wails via Go, ensure you run the following commands to update your PATH: +
+ export PATH=$PATH:$(go env GOPATH)/bin +
+ source ~/.bashrc or source ~/.zshrc +
+
+``` + +## Optional Dependencies + +- [UPX](https://upx.github.io/) for compressing your applications. +- [NSIS](https://wails.io/docs/guides/windows-installer/) for generating Windows installers. + +## Installing Wails + +Run `go install github.com/wailsapp/wails/v2/cmd/wails@latest` to install the Wails CLI. + +Note: If you get an error similar to this: + +```shell +....\Go\pkg\mod\github.com\wailsapp\wails\v2@v2.1.0\pkg\templates\templates.go:28:12: pattern all:ides/*: no matching files found +``` + +please check you have Go 1.18+ installed: + +```shell +go version +``` + +## System Check + +Running `wails doctor` will check if you have the correct dependencies installed. If not, it will advise on what is missing and help on how to rectify any problems. + +## The `wails` command appears to be missing? + +If your system is reporting that the `wails` command is missing, make sure you have followed the Go installation guide +correctly. Normally, it means that the `go/bin` directory in your User's home directory is not in the `PATH` environment +variable. You will also normally need to close and reopen any open command prompts so that changes to the environment +made by the installer are reflected at the command prompt. diff --git a/website/docs/guides/_category_.json b/website/docs/guides/_category_.json new file mode 100644 index 000000000..5935dad93 --- /dev/null +++ b/website/docs/guides/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 50 +} diff --git a/website/docs/guides/angular.mdx b/website/docs/guides/angular.mdx new file mode 100644 index 000000000..92eec68d5 --- /dev/null +++ b/website/docs/guides/angular.mdx @@ -0,0 +1,14 @@ +# Angular + +Whilst Wails does not have an Angular template, it is possible to use Angular with Wails. + +## Dev Mode + +To get dev mode working with Angular, you need to add the following to your `wails.json`: + +```json + "frontend:build": "npx ng build", + "frontend:install": "npm install", + "frontend:dev:watcher": "npx ng serve", + "frontend:dev:serverUrl": "http://localhost:4200", +``` \ No newline at end of file diff --git a/website/docs/guides/application-development.mdx b/website/docs/guides/application-development.mdx new file mode 100644 index 000000000..adefa4b04 --- /dev/null +++ b/website/docs/guides/application-development.mdx @@ -0,0 +1,317 @@ +# Application Development + +There are no hard and fast rules for developing applications with Wails, but there are some basic guidelines. + +## Application Setup + +The pattern used by the default templates are that `main.go` is used for configuring and running the application, whilst +`app.go` is used for defining the application logic. + +The `app.go` file will define a struct that has 2 methods which act as hooks into the main application: + +```go title="app.go" +import ( + "context" +) + +type App struct { + ctx context.Context +} + +func NewApp() *App { + return &App{} +} + +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +func (a *App) shutdown(ctx context.Context) { +} +``` + +- The startup method is called as soon as Wails allocates the resources it needs and is a good place for creating resources, + setting up event listeners and anything else the application needs at startup. + It is given a [`context.Context`](https://pkg.go.dev/context) which is usually saved in a struct field. This context is needed for calling the + [runtime](../reference/runtime/intro.mdx). If this method returns an error, the application will terminate. + In dev mode, the error will be output to the console. + +- The shutdown method will be called by Wails right at the end of the shutdown process. This is a good place to deallocate + memory and perform any shutdown tasks. + +The `main.go` file generally consists of a single call to `wails.Run()`, which accepts the application configuration. +The pattern used by the templates is that before the call to `wails.Run()`, an instance of the struct we defined in +`app.go` is created and saved in a variable called `app`. This configuration is where we add our callbacks: + +```go {3,9,10} title="main.go" +func main() { + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: app.startup, + OnShutdown: app.shutdown, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +More information on application lifecycle hooks can be found [here](../howdoesitwork.mdx#application-lifecycle-callbacks). + +## Binding Methods + +It is likely that you will want to call Go methods from the frontend. This is normally done by adding public methods to +the already defined struct in `app.go`: + +```go {3,21-23} title="app.go" +import ( + "context" + "fmt" +) + +type App struct { + ctx context.Context +} + +func NewApp() *App { + return &App{} +} + +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +func (a *App) shutdown(ctx context.Context) { +} + +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s!", name) +} +``` + +In the main application configuration, the `Bind` key is where we can tell Wails what we want to bind: + +```go {11-13} title="main.go" +func main() { + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + }, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +This will bind all public methods in our `App` struct (it will never bind the startup and shutdown methods). + +### Dealing with context when binding multiple structs + +If you want to bind methods for multiple structs but want each struct to keep a reference to the context so that you +can use the runtime functions, a good pattern is to pass the context from the `OnStartup` method to your struct instances +: + +```go +func main() { + + app := NewApp() + otherStruct := NewOtherStruct() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: func(ctx context.Context){ + app.SetContext(ctx) + otherStruct.SetContext(ctx) + }, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + otherStruct + }, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +Also you might want to use Enums in your structs and have models for them on frontend. +In that case you should create array that will contain all possible enum values, instrument enum type and bind it to the app: + +```go {16-18} title="app.go" +type Weekday string + +const ( + Sunday Weekday = "Sunday" + Monday Weekday = "Monday" + Tuesday Weekday = "Tuesday" + Wednesday Weekday = "Wednesday" + Thursday Weekday = "Thursday" + Friday Weekday = "Friday" + Saturday Weekday = "Saturday" +) + +var AllWeekdays = []struct { + Value Weekday + TSName string +}{ + {Sunday, "SUNDAY"}, + {Monday, "MONDAY"}, + {Tuesday, "TUESDAY"}, + {Wednesday, "WEDNESDAY"}, + {Thursday, "THURSDAY"}, + {Friday, "FRIDAY"}, + {Saturday, "SATURDAY"}, +} +``` + +In the main application configuration, the `EnumBind` key is where we can tell Wails what we want to bind enums as well: + +```go {11-13} title="main.go" +func main() { + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + }, + EnumBind: []interface{}{ + AllWeekdays, + }, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +This will add missing enums to your `model.ts` file. + +More information on Binding can be found [here](../howdoesitwork.mdx#method-binding). + +## Application Menu + +Wails supports adding a menu to your application. This is done by passing a [Menu](../reference/menus.mdx#menu) struct +to application config. It's common to use a method that returns a Menu, and even more common for that to be a method on +the `App` struct used for the lifecycle hooks. + +```go {11} title="main.go" +func main() { + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Menu: app.menu(), + Bind: []interface{}{ + app, + }, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +## Assets + +The great thing about the way Wails v2 handles assets is that it doesn't! The only thing you need to give Wails is an +`embed.FS`. How you get to that is entirely up to you. You can use vanilla html/css/js files like the vanilla template. +You could have some complicated build system, it doesn't matter. + +When `wails build` is run, it will check the `wails.json` project file at the project root. There are 2 keys in the +project file that are read: + +- "frontend:install" +- "frontend:build" + +The first, if given, will be executed in the `frontend` directory to install the node modules. +The second, if given, will be executed in the `frontend` directory to build the frontend project. + +If these 2 keys aren't given, then Wails does absolutely nothing with the frontend. It is only expecting that `embed.FS`. + +### AssetsHandler + +A Wails v2 app can optionally define a `http.Handler` in the `options.App`, which allows hooking into the AssetServer to +create files on the fly or process POST/PUT requests. +GET requests are always first handled by the `assets` FS. If the FS doesn't find the requested file the request will be +forwarded to the `http.Handler` for serving. Any requests other than GET will be directly processed by the `AssetsHandler` +if specified. +It's also possible to only use the `AssetsHandler` by specifying `nil` as the `Assets` option. + +## Built in Dev Server + +Running `wails dev` will start the built in dev server which will start a file watcher in your project directory. By +default, if any file changes, wails checks if it was an application file (default: `.go`, configurable with `-e` flag). +If it was, then it will rebuild your application and relaunch it. If the changed file was in the assets, +it will issue a reload after a short amount of time. + +The dev server uses a technique called "debouncing" which means it doesn't reload straight away, +as there may be multiple files changed in a short amount of time. When a trigger occurs, it waits for a set amount of time +before issuing a reload. If another trigger happens, it resets to the wait time again. By default this value is `100ms`. +If this value doesn't work for your project, it can be configured using the `-debounce` flag. If used, this value will +be saved to your project config and become the default. + +## External Dev Server + +Some frameworks come with their own live-reloading server, however they will not be able to take advantage of the Wails +Go bindings. In this scenario, it is best to run a watcher script that rebuilds the project into the build directory, which +Wails will be watching. For an example, see the default svelte template that uses [rollup](https://rollupjs.org/guide/en/). + +### Create React App + +The process for a Create-React-App project is slightly more complicated. In order to support live frontend reloading the following configuration +needs to be added to your `wails.json`: + +```json + "frontend:dev:watcher": "yarn start", + "frontend:dev:serverUrl": "http://localhost:3000", +``` + +The `frontend:dev:watcher` command will start the Create-React-App development server (hosted on port `3000` typically). The `frontend:dev:serverUrl` command then +instructs Wails to serve assets from the development server when loading the frontend rather than from the build folder. In addition to the above, the +`index.html` needs to be updated with the following: + +```html + + + + + +``` + +This is required as the watcher command that rebuilds the frontend prevents Wails from injecting the required scripts. This circumvents that issue by ensuring +the scripts are always injected. With this configuration, `wails dev` can be run which will appropriately build the frontend and backend with hot-reloading enabled. +Additionally, when accessing the application from a browser the React developer tools can now be used on a non-minified version of the application for straightforward +debugging. Finally, for faster builds, `wails dev -s` can be run to skip the default building of the frontend by Wails as this is an unnecessary step. + +## Go Module + +The default Wails templates generate a `go.mod` file that contains the module name "changeme". You should change this +to something more appropriate after project generation. diff --git a/website/docs/guides/crossplatform-build.mdx b/website/docs/guides/crossplatform-build.mdx new file mode 100644 index 000000000..f6fbc0f06 --- /dev/null +++ b/website/docs/guides/crossplatform-build.mdx @@ -0,0 +1,65 @@ +# Crossplatform build with Github Actions + +To build a Wails project for all the available platforms, you need to create an application build for each operating system. One effective method to achieve this is by utilizing GitHub Actions. + +An action that facilitates building a Wails app is available at: +https://github.com/dAppServer/wails-build-action + +In case the existing action doesn't fulfill your requirements, you can select only the necessary steps from the source: +https://github.com/dAppServer/wails-build-action/blob/main/action.yml + +Below is a comprehensive example that demonstrates building an app upon the creation of a new Git tag and subsequently uploading it to the Actions artifacts: + +```yaml +name: Wails build + +on: + push: + tags: + # Match any new tag + - '*' + +env: + # Necessary for most environments as build failure can occur due to OOM issues + NODE_OPTIONS: "--max-old-space-size=4096" + +jobs: + build: + strategy: + # Failure in one platform build won't impact the others + fail-fast: false + matrix: + build: + - name: 'App' + platform: 'linux/amd64' + os: 'ubuntu-latest' + - name: 'App' + platform: 'windows/amd64' + os: 'windows-latest' + - name: 'App' + platform: 'darwin/universal' + os: 'macos-latest' + + runs-on: ${{ matrix.build.os }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Build wails + uses: dAppServer/wails-build-action@v2.2 + id: build + with: + build-name: ${{ matrix.build.name }} + build-platform: ${{ matrix.build.platform }} + package: false + go-version: '1.20' +``` + +This example offers opportunities for various enhancements, including: +- Caching dependencies +- Code signing +- Uploading to platforms like S3, Supabase, etc. +- Injecting secrets as environment variables +- Utilizing environment variables as build variables (such as version variable extracted from the current Git tag) diff --git a/website/docs/guides/custom-protocol-schemes.mdx b/website/docs/guides/custom-protocol-schemes.mdx new file mode 100644 index 000000000..216fb7100 --- /dev/null +++ b/website/docs/guides/custom-protocol-schemes.mdx @@ -0,0 +1,207 @@ +# Custom Protocol Scheme association + +Custom Protocols feature allows you to associate specific custom protocol with your app so that when users open links with this protocol, +your app is launched to handle them. This can be particularly useful to connect your desktop app with your web app. +In this guide, we'll walk through the steps to implement custom protocols in Wails app. + + +## Set Up Custom Protocol Schemes Association: +To set up custom protocol, you need to modify your application's wails.json file. +In "info" section add a "protocols" section specifying the protocols your app should be associated with. + +For example: +```json +{ + "info": { + "protocols": [ + { + "scheme": "myapp", + "description": "My App Protocol", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:--------------------------------------------------------------------------------------| +| scheme | Custom Protocol scheme. e.g. myapp | +| description | Windows-only. The description. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open custom protocol with your app, the system will launch your app and call the `OnUrlOpen` function in your Wails app. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + Mac: &mac.Options{ + OnUrlOpen: func(url string) { println(url) }, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + +If you want to handle universal links as well, follow this [guide](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) to add required entitlements, add required keys to Info.plist and configure `apple-app-site-association` on your website. + +Here is example for Info.plist: +```xml +NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + +``` + +And for entitlements.plist +```xml +com.apple.developer.associated-domains + + applinks:myawesomeapp.com + +``` + + +### Windows +On Windows Custom Protocol Schemes is supported only with NSIS installer. During installation, the installer will create a +registry entry for your schemes. When you open url with your app, new instance of app is launched and url is passed +as argument to your app. To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` + +### Linux +Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually. +For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle. +You can use [nfpm](https://nfpm.goreleaser.com/) to create .deb package for your app. + +1. Create a .desktop file for your app and specify file associations there (note that `%u` is important in Exec). Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file %u +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=x-scheme-handler/myapp; +``` + +2. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +``` +3. Configure nfpm to use your scripts and files. Example: +```yaml +name: "wails-open-file" +arch: "arm64" +platform: "linux" +version: "1.0.0" +section: "default" +priority: "extra" +maintainer: "FooBarCorp " +description: "Sample Package" +vendor: "FooBarCorp" +homepage: "http://example.com" +license: "MIT" +contents: +- src: ../bin/wails-open-file + dst: /usr/bin/wails-open-file +- src: ./main.desktop + dst: /usr/share/applications/wails-open-file.desktop +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +# copy icons to Yaru theme as well. For some reason Ubuntu didn't pick up fileicons from hicolor theme +- src: ../appicon.svg + dst: /usr/share/icons/Yaru/scalable/apps/wails-open-file.svg +scripts: + postinstall: ./postInstall.sh + postremove: ./postRemove.sh +``` +6. Build your .deb package using nfpm: +```sh +nfpm pkg --packager deb --target . +``` +7. Now when your package is installed, your app will be associated with custom protocol scheme. When you open url with your app, +new instance of app is launched and file path is passed as argument to your app. +To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/docs/guides/dynamic-assets.mdx b/website/docs/guides/dynamic-assets.mdx new file mode 100644 index 000000000..8d1debcef --- /dev/null +++ b/website/docs/guides/dynamic-assets.mdx @@ -0,0 +1,153 @@ +# Dynamic Assets + +:::info + +This does not work with vite v5.0.0+ and wails v2 due to changes in vite. +Changes are planned in v3 to support similar functionality under vite v5.0.0+. +If you need this feature, stay with vite v4.0.0+. +See [issue 3240](https://github.com/wailsapp/wails/issues/3240) for details + +::: + +If you want to load or generate assets for your frontend dynamically, you can achieve that using the +[AssetsHandler](../reference/options#assetshandler) option. The AssetsHandler is a generic `http.Handler` which will +be called for any non GET request on the assets server and for GET requests which can not be served from the +bundled assets because the file is not found. + +By installing a custom AssetsHandler, you can serve your own assets using a custom asset server. + +## Example + +In our example project, we will create a simple assets handler which will load files off disk: + +```go title=main.go {17-36,49} +package main + +import ( + "embed" + "fmt" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "net/http" + "os" + "strings" +) + +//go:embed all:frontend/dist +var assets embed.FS + +type FileLoader struct { + http.Handler +} + +func NewFileLoader() *FileLoader { + return &FileLoader{} +} + +func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) { + var err error + requestedFilename := strings.TrimPrefix(req.URL.Path, "/") + println("Requesting file:", requestedFilename) + fileData, err := os.ReadFile(requestedFilename) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + res.Write([]byte(fmt.Sprintf("Could not load file %s", requestedFilename))) + } + + res.Write(fileData) +} + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "helloworld", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + Handler: NewFileLoader(), + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 255}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err) + } +} +``` + +When we run the application in dev mode using `wails dev`, we will see the following output: + +``` +DEB | [ExternalAssetHandler] Loading 'http://localhost:3001/favicon.ico' +DEB | [ExternalAssetHandler] Loading 'http://localhost:3001/favicon.ico' failed, using AssetHandler +Requesting file: favicon.ico +``` + +As you can see, the assets handler is called when the default assets server is unable to serve +the `favicon.ico` file. + +If you right click the main application and select "inspect" to bring up the devtools, you can test +this feature out by typing the following into the console: + +``` +let response = await fetch('does-not-exist.txt'); +``` + +This will generate an error in the devtools. We can see that the error is what we expect, returned by +our custom assets handler: + +```mdx-code-block +

+ +

+``` + +However, if we request `go.mod`, we will see the following output: + +```mdx-code-block +

+ +

+``` + +This technique can be used to load images directly into the page. If we updated our default vanilla template and +replaced the logo image: + +```html + +``` + +with: + +```html + +``` + +Then we would see the following: + +```mdx-code-block +

+ +

+``` + +:::warning + +Exposing your filesystem in this way is a security risk. It is recommended that you properly manage access +to your filesystem. + +::: diff --git a/website/docs/guides/file-association.mdx b/website/docs/guides/file-association.mdx new file mode 100644 index 000000000..71bbff37e --- /dev/null +++ b/website/docs/guides/file-association.mdx @@ -0,0 +1,228 @@ +# File Association + +File association feature allows you to associate specific file types with your app so that when users open those files, +your app is launched to handle them. This can be particularly useful for text editors, image viewers, or any application +that works with specific file formats. In this guide, we'll walk through the steps to implement file association in Wails app. + + +## Set Up File Association: +To set up file association, you need to modify your application's wails.json file. +In "info" section add a "fileAssociations" section specifying the file types your app should be associated with. + +For example: +```json +{ + "info": { + "fileAssociations": [ + { + "ext": "wails", + "name": "Wails", + "description": "Wails Application File", + "iconName": "wailsFileIcon", + "role": "Editor" + }, + { + "ext": "jpg", + "name": "JPEG", + "description": "Image File", + "iconName": "jpegFileIcon", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| ext | The extension (minus the leading period). e.g. png | +| name | The name. e.g. PNG File | +| iconName | The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows | +| description | Windows-only. The description. It is displayed on the `Type` column on Windows Explorer. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open file (or files) with your app, the system will launch your app and call the `OnFileOpen` function in your Wails app. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + Mac: &mac.Options{ + OnFileOpen: func(filePaths []string) { println(filestring) }, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + + +### Windows +On Windows file association is supported only with NSIS installer. During installation, the installer will create a +registry entry for your file associations. When you open file with your app, new instance of app is launched and file path is passed +as argument to your app. To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` + +### Linux +Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually. +For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle. +You can use [nfpm](https://nfpm.goreleaser.com/) to create .deb package for your app. + +1. Create a .desktop file for your app and specify file associations there. Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file %u +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=application/x-wails;application/x-test +``` + +2. Create mime types file. Example: +```xml + + + + Wails Application File + + + +``` + +3. Create icons for your file types. SVG icons are recommended. +4. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload mime types to register file associations +update-mime-database /usr/share/mime +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +# update icons +update-icon-caches /usr/share/icons/* +``` +5. Configure nfpm to use your scripts and files. Example: +```yaml +name: "wails-open-file" +arch: "arm64" +platform: "linux" +version: "1.0.0" +section: "default" +priority: "extra" +maintainer: "FooBarCorp " +description: "Sample Package" +vendor: "FooBarCorp" +homepage: "http://example.com" +license: "MIT" +contents: +- src: ../bin/wails-open-file + dst: /usr/bin/wails-open-file +- src: ./main.desktop + dst: /usr/share/applications/wails-open-file.desktop +- src: ./application-wails-mime.xml + dst: /usr/share/mime/packages/application-x-wails.xml +- src: ./application-test-mime.xml + dst: /usr/share/mime/packages/application-x-test.xml +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-test.svg +# copy icons to Yaru theme as well. For some reason Ubuntu didn't pick up fileicons from hicolor theme +- src: ../appicon.svg + dst: /usr/share/icons/Yaru/scalable/apps/wails-open-file.svg +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-test.svg +scripts: + postinstall: ./postInstall.sh + postremove: ./postRemove.sh +``` +6. Build your .deb package using nfpm: +```sh +nfpm pkg --packager deb --target . +``` +7. Now when your package is installed, your app will be associated with specified file types. When you open file with your app, +new instance of app is launched and file path is passed as argument to your app. +To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/docs/guides/frameless.mdx b/website/docs/guides/frameless.mdx new file mode 100644 index 000000000..07d8d2d25 --- /dev/null +++ b/website/docs/guides/frameless.mdx @@ -0,0 +1,92 @@ +# Frameless Applications + +Wails supports application that have no frames. This can be achieved by using the [frameless](../reference/options.mdx#frameless) +field in [Application Options](../reference/options.mdx#application-options). + +Wails offers a simple solution for dragging the window: Any HTML element that has the CSS style `--wails-draggable:drag` will +act as a "drag handle". This property applies to all child elements. If you need to indicate that a nested element +should not drag, then use the attribute '--wails-draggable:no-drag' on that element. + +```html + + + + + + + +
+ + +
+
+ + + + +``` + +For some projects, using a CSS variable may not be possible due to dynamic styling. In this case, you can use the +`CSSDragProperty` and `CSSDragValue` application options to define a property and value that will be used to indicate +draggable regions: + +```go title=main.go +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "alwaysontop", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + Frameless: true, + CSSDragProperty: "widows", + CSSDragValue: "1", + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err) + } +} +``` + +```html title=index.html + + + + + + alwaysontop + + +
+ + + +``` + +:::info Fullscreen + +If you allow your application to go fullscreen, this drag functionality will be disabled. + +::: diff --git a/website/docs/guides/frontend.mdx b/website/docs/guides/frontend.mdx new file mode 100644 index 000000000..2c3c78e42 --- /dev/null +++ b/website/docs/guides/frontend.mdx @@ -0,0 +1,73 @@ +# Frontend + +## Script Injection + +When Wails serves your `index.html`, by default, it will inject 2 script entries into the `` tag to load `/wails/ipc.js` +and `/wails/runtime.js`. These files install the bindings and runtime respectively. + +The code below shows where these are injected by default: + +```html + + + injection example + + + + + + + +
Please enter your name below 👇
+
+ + +
+ + + + +``` + +### Overriding Default Script Injection + +To provide more flexibility to developers, there is a meta tag that may be used to customise this behaviour: + +```html + +``` + +The options are as follows: + +| Value | Description | +| ------------------- | ------------------------------------------------ | +| noautoinjectruntime | Disable the autoinjection of `/wails/runtime.js` | +| noautoinjectipc | Disable the autoinjection of `/wails/ipc.js` | +| noautoinject | Disable all autoinjection of scripts | + +Multiple options may be used provided they are comma separated. + +This code is perfectly valid and operates the same as the autoinjection version: + +```html + + + injection example + + + + + + +
Please enter your name below 👇
+
+ + +
+ + + + + + +``` diff --git a/website/docs/guides/ides.mdx b/website/docs/guides/ides.mdx new file mode 100644 index 000000000..5fe5a7bb9 --- /dev/null +++ b/website/docs/guides/ides.mdx @@ -0,0 +1,131 @@ +# IDEs + +Wails aims to provide a great development experience. To that aim, we now support generating IDE specific configuration +to provide smoother project setup. + +Currently, we support [Visual Studio Code](https://code.visualstudio.com/) and [Goland](https://www.jetbrains.com/go/). + +## Visual Studio Code + +```mdx-code-block +

+ +

+``` + +When generating a project using the `-ide vscode` flags, IDE files will be created alongside the other project files. +These files are placed into the `.vscode` directory and provide the correct configuration for debugging your application. + +The 2 files generated are `tasks.json` and `launch.json`. Below are the files generated for the default vanilla project: + +```json title="tasks.json" +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}" + }, + "command": "go", + "args": [ + "build", + "-tags", + "dev", + "-gcflags", + "all=-N -l", + "-o", + "build/bin/myproject.exe" + ] + } + ] +} +``` + +```json title="launch.json" +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Wails: Debug myproject", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/build/bin/myproject.exe", + "preLaunchTask": "build", + "cwd": "${workspaceFolder}", + "env": {} + } + ] +} +``` + +### Configuring the install and build steps + +The `tasks.json` file is simple for the default project as there is no `npm install` or `npm run build` step needed. +For projects that have a frontend build step, such as the svelte template, we would need to edit `tasks.json` to +add the install and build steps: + +```json title="tasks.json" +{ + "version": "2.0.0", + "tasks": [ + { + "label": "npm install", + "type": "npm", + "script": "install", + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "presentation": { + "clear": true, + "panel": "shared", + "showReuseMessage": false + }, + "problemMatcher": [] + }, + { + "label": "npm run build", + "type": "npm", + "script": "build", + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "presentation": { + "clear": true, + "panel": "shared", + "showReuseMessage": false + }, + "problemMatcher": [] + }, + { + "label": "build", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}" + }, + "command": "go", + "args": [ + "build", + "-tags", + "dev", + "-gcflags", + "all=-N -l", + "-o", + "build/bin/vscode.exe" + ], + "dependsOn": ["npm install", "npm run build"] + } + ] +} +``` + +:::info Future Enhancement + +In the future, we hope to generate a `tasks.json` that includes the install and build steps automatically. + +::: diff --git a/website/docs/guides/linux-distro-support.mdx b/website/docs/guides/linux-distro-support.mdx new file mode 100644 index 000000000..b64ed0c03 --- /dev/null +++ b/website/docs/guides/linux-distro-support.mdx @@ -0,0 +1,110 @@ +# Linux Distro Support + +## Overview + +Wails offers Linux support but providing installation instructions for all available distributions is an impossible task. +Instead, Wails tries to determine if the packages you need to develop applications are available via your system's package +manager. Currently, we support the following package managers: + +- apt +- dnf +- emerge +- eopkg +- nixpkgs +- pacman +- zypper + +## Adding package names + +There may be circumstances where your distro uses one of the supported package managers but the package name +is different. For example, you may use an Ubuntu derivative, but the package name for gtk may be different. Wails +attempts to find the correct package by iterating through a list of package names. +The list of packages are stored in the packagemanager specific file in the `v2/internal/system/packagemanager` +directory. In our example, this would be `v2/internal/system/packagemanager/apt.go`. + +In this file, the list of packages are defined by the `Packages()` method: + +```go +func (a *Apt) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-dev", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "build-essential", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker.io", SystemPackage: true, Optional: true}, + }, + } +} +``` + +Let's assume that in our linux distro, `libgtk-3` is packaged under the name `lib-gtk3-dev`. +We could add support for this by adding the following line: + +```go {5} +func (a *Apt) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-dev", SystemPackage: true, Library: true}, + {Name: "lib-gtk3-dev", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "build-essential", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker.io", SystemPackage: true, Optional: true}, + }, + } +} +``` + +## Adding new package managers + +To add a new package manager, perform the following steps: + +- Create a new file in `v2/internal/system/packagemanager` called `.go`, where `` is the name of the package manager. +- Define a struct that conforms to the package manager interface defined in `pm.go`: + +```go +type PackageManager interface { + Name() string + Packages() packagemap + PackageInstalled(*Package) (bool, error) + PackageAvailable(*Package) (bool, error) + InstallCommand(*Package) string +} +``` + +- `Name()` should return the name of the package manager +- `Packages()` should return a `packagemap`, that provides candidate filenames for dependencies +- `PackageInstalled()` should return `true` if the given package is installed +- `PackageAvailable()` should return `true` if the given package is not installed but available for installation +- `InstallCommand()` should return the exact command to install the given package name + +Take a look at the other package managers code to get an idea how this works. + +:::info Remember + +If you add support for a new package manager, don't forget to also update this page! + +::: diff --git a/website/docs/guides/linux.mdx b/website/docs/guides/linux.mdx new file mode 100644 index 000000000..2cfc2e62a --- /dev/null +++ b/website/docs/guides/linux.mdx @@ -0,0 +1,126 @@ +# Linux + +This page has miscellaneous guides related to developing Wails applications for Linux. + +## Video tag doesn't fire "ended" event + +When using a video tag, the "ended" event is not fired when the video is finished playing. This is a bug +in WebkitGTK, however you can use the following workaround to fix it: + +```js +videoTag.addEventListener("timeupdate", (event) => { + if (event.target.duration - event.target.currentTime < 0.2) { + let ended = new Event("ended"); + event.target.dispatchEvent(ended); + } +}); +``` + +Source: [Lyimmi](https://github.com/Lyimmi) on the +[discussions board](https://github.com/wailsapp/wails/issues/1729#issuecomment-1212291275) + +## GStreamer error when using Audio or Video elements + +If you are seeing the following error when including `