diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6d15b9e..6954097 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -77,6 +77,19 @@
"module": "pbsync",
"console": "integratedTerminal"
},
+ {
+ "name": "GUI",
+ "args": [
+ "--gui",
+ "--debugpath",
+ "${input:debugPath}"
+ ],
+ "type": "python",
+ "justMyCode": false,
+ "request": "launch",
+ "module": "pbsync",
+ "console": "integratedTerminal"
+ },
{
"name": "Pull Binaries",
"args": ["--sync", "binaries", "--debugpath", "${input:debugPath}"],
diff --git a/gui/__init__.py b/gui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/css/__init__.py b/gui/css/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/css/app.css b/gui/css/app.css
new file mode 100644
index 0000000..bcee31e
--- /dev/null
+++ b/gui/css/app.css
@@ -0,0 +1,58 @@
+#app {
+ height: 100vh;
+ overflow-y: auto;
+ background-color: var(--bs-dark);
+ color: var(--bs-light);
+}
+
+#app-inner {
+ margin-top: 1rem;
+}
+
+.flx-LineEdit {
+ width: 100%;
+ border-radius: 0.25rem;
+}
+
+.nav-pills .flx-Button {
+ text-align: start;
+}
+
+.flx-BaseButton {
+ color: inherit;
+}
+
+.flx-Widget:not(.flx-Layout) > .flx-Layout {
+ position: initial;
+}
+
+.flx-box {
+ justify-content: initial;
+}
+
+.flx-Label {
+ align-self: center;
+}
+
+.fa-check-circle {
+ color: var(--bs-success);
+}
+
+.table-danger .fa-times-circle {
+ color: inherit;
+}
+
+.fa-times-circle {
+ color: var(--bs-danger);
+}
+
+.commit-dropdown > .btn {
+ line-height: 1;
+ padding: .15rem .75rem;
+ visibility: hidden;
+}
+
+tr:hover .commit-dropdown > .btn,
+tr.active .commit-dropdown > .btn {
+ visibility: visible;
+}
diff --git a/gui/css/font-awesome.css b/gui/css/font-awesome.css
new file mode 100644
index 0000000..5f84c95
--- /dev/null
+++ b/gui/css/font-awesome.css
@@ -0,0 +1,4619 @@
+/*!
+ * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
+ * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+ */
+.fa,
+.fas,
+.far,
+.fal,
+.fad,
+.fab {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ display: inline-block;
+ font-style: normal;
+ font-variant: normal;
+ text-rendering: auto;
+ line-height: 1; }
+
+.fa-lg {
+ font-size: 1.33333em;
+ line-height: 0.75em;
+ vertical-align: -.0667em; }
+
+.fa-xs {
+ font-size: .75em; }
+
+.fa-sm {
+ font-size: .875em; }
+
+.fa-1x {
+ font-size: 1em; }
+
+.fa-2x {
+ font-size: 2em; }
+
+.fa-3x {
+ font-size: 3em; }
+
+.fa-4x {
+ font-size: 4em; }
+
+.fa-5x {
+ font-size: 5em; }
+
+.fa-6x {
+ font-size: 6em; }
+
+.fa-7x {
+ font-size: 7em; }
+
+.fa-8x {
+ font-size: 8em; }
+
+.fa-9x {
+ font-size: 9em; }
+
+.fa-10x {
+ font-size: 10em; }
+
+.fa-fw {
+ text-align: center;
+ width: 1.25em; }
+
+.fa-ul {
+ list-style-type: none;
+ margin-left: 2.5em;
+ padding-left: 0; }
+ .fa-ul > li {
+ position: relative; }
+
+.fa-li {
+ left: -2em;
+ position: absolute;
+ text-align: center;
+ width: 2em;
+ line-height: inherit; }
+
+.fa-border {
+ border: solid 0.08em #eee;
+ border-radius: .1em;
+ padding: .2em .25em .15em; }
+
+.fa-pull-left {
+ float: left; }
+
+.fa-pull-right {
+ float: right; }
+
+.fa.fa-pull-left,
+.fas.fa-pull-left,
+.far.fa-pull-left,
+.fal.fa-pull-left,
+.fab.fa-pull-left {
+ margin-right: .3em; }
+
+.fa.fa-pull-right,
+.fas.fa-pull-right,
+.far.fa-pull-right,
+.fal.fa-pull-right,
+.fab.fa-pull-right {
+ margin-left: .3em; }
+
+.fa-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear; }
+
+.fa-pulse {
+ -webkit-animation: fa-spin 1s infinite steps(8);
+ animation: fa-spin 1s infinite steps(8); }
+
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg); }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg); } }
+
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg); }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg); } }
+
+.fa-rotate-90 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg); }
+
+.fa-rotate-180 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+ -webkit-transform: rotate(180deg);
+ transform: rotate(180deg); }
+
+.fa-rotate-270 {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+ -webkit-transform: rotate(270deg);
+ transform: rotate(270deg); }
+
+.fa-flip-horizontal {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+ -webkit-transform: scale(-1, 1);
+ transform: scale(-1, 1); }
+
+.fa-flip-vertical {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+ -webkit-transform: scale(1, -1);
+ transform: scale(1, -1); }
+
+.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical {
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+ -webkit-transform: scale(-1, -1);
+ transform: scale(-1, -1); }
+
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical,
+:root .fa-flip-both {
+ -webkit-filter: none;
+ filter: none; }
+
+.fa-stack {
+ display: inline-block;
+ height: 2em;
+ line-height: 2em;
+ position: relative;
+ vertical-align: middle;
+ width: 2.5em; }
+
+.fa-stack-1x,
+.fa-stack-2x {
+ left: 0;
+ position: absolute;
+ text-align: center;
+ width: 100%; }
+
+.fa-stack-1x {
+ line-height: inherit; }
+
+.fa-stack-2x {
+ font-size: 2em; }
+
+.fa-inverse {
+ color: #fff; }
+
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+readers do not read off random characters that represent icons */
+.fa-500px:before {
+ content: "\f26e"; }
+
+.fa-accessible-icon:before {
+ content: "\f368"; }
+
+.fa-accusoft:before {
+ content: "\f369"; }
+
+.fa-acquisitions-incorporated:before {
+ content: "\f6af"; }
+
+.fa-ad:before {
+ content: "\f641"; }
+
+.fa-address-book:before {
+ content: "\f2b9"; }
+
+.fa-address-card:before {
+ content: "\f2bb"; }
+
+.fa-adjust:before {
+ content: "\f042"; }
+
+.fa-adn:before {
+ content: "\f170"; }
+
+.fa-adversal:before {
+ content: "\f36a"; }
+
+.fa-affiliatetheme:before {
+ content: "\f36b"; }
+
+.fa-air-freshener:before {
+ content: "\f5d0"; }
+
+.fa-airbnb:before {
+ content: "\f834"; }
+
+.fa-algolia:before {
+ content: "\f36c"; }
+
+.fa-align-center:before {
+ content: "\f037"; }
+
+.fa-align-justify:before {
+ content: "\f039"; }
+
+.fa-align-left:before {
+ content: "\f036"; }
+
+.fa-align-right:before {
+ content: "\f038"; }
+
+.fa-alipay:before {
+ content: "\f642"; }
+
+.fa-allergies:before {
+ content: "\f461"; }
+
+.fa-amazon:before {
+ content: "\f270"; }
+
+.fa-amazon-pay:before {
+ content: "\f42c"; }
+
+.fa-ambulance:before {
+ content: "\f0f9"; }
+
+.fa-american-sign-language-interpreting:before {
+ content: "\f2a3"; }
+
+.fa-amilia:before {
+ content: "\f36d"; }
+
+.fa-anchor:before {
+ content: "\f13d"; }
+
+.fa-android:before {
+ content: "\f17b"; }
+
+.fa-angellist:before {
+ content: "\f209"; }
+
+.fa-angle-double-down:before {
+ content: "\f103"; }
+
+.fa-angle-double-left:before {
+ content: "\f100"; }
+
+.fa-angle-double-right:before {
+ content: "\f101"; }
+
+.fa-angle-double-up:before {
+ content: "\f102"; }
+
+.fa-angle-down:before {
+ content: "\f107"; }
+
+.fa-angle-left:before {
+ content: "\f104"; }
+
+.fa-angle-right:before {
+ content: "\f105"; }
+
+.fa-angle-up:before {
+ content: "\f106"; }
+
+.fa-angry:before {
+ content: "\f556"; }
+
+.fa-angrycreative:before {
+ content: "\f36e"; }
+
+.fa-angular:before {
+ content: "\f420"; }
+
+.fa-ankh:before {
+ content: "\f644"; }
+
+.fa-app-store:before {
+ content: "\f36f"; }
+
+.fa-app-store-ios:before {
+ content: "\f370"; }
+
+.fa-apper:before {
+ content: "\f371"; }
+
+.fa-apple:before {
+ content: "\f179"; }
+
+.fa-apple-alt:before {
+ content: "\f5d1"; }
+
+.fa-apple-pay:before {
+ content: "\f415"; }
+
+.fa-archive:before {
+ content: "\f187"; }
+
+.fa-archway:before {
+ content: "\f557"; }
+
+.fa-arrow-alt-circle-down:before {
+ content: "\f358"; }
+
+.fa-arrow-alt-circle-left:before {
+ content: "\f359"; }
+
+.fa-arrow-alt-circle-right:before {
+ content: "\f35a"; }
+
+.fa-arrow-alt-circle-up:before {
+ content: "\f35b"; }
+
+.fa-arrow-circle-down:before {
+ content: "\f0ab"; }
+
+.fa-arrow-circle-left:before {
+ content: "\f0a8"; }
+
+.fa-arrow-circle-right:before {
+ content: "\f0a9"; }
+
+.fa-arrow-circle-up:before {
+ content: "\f0aa"; }
+
+.fa-arrow-down:before {
+ content: "\f063"; }
+
+.fa-arrow-left:before {
+ content: "\f060"; }
+
+.fa-arrow-right:before {
+ content: "\f061"; }
+
+.fa-arrow-up:before {
+ content: "\f062"; }
+
+.fa-arrows-alt:before {
+ content: "\f0b2"; }
+
+.fa-arrows-alt-h:before {
+ content: "\f337"; }
+
+.fa-arrows-alt-v:before {
+ content: "\f338"; }
+
+.fa-artstation:before {
+ content: "\f77a"; }
+
+.fa-assistive-listening-systems:before {
+ content: "\f2a2"; }
+
+.fa-asterisk:before {
+ content: "\f069"; }
+
+.fa-asymmetrik:before {
+ content: "\f372"; }
+
+.fa-at:before {
+ content: "\f1fa"; }
+
+.fa-atlas:before {
+ content: "\f558"; }
+
+.fa-atlassian:before {
+ content: "\f77b"; }
+
+.fa-atom:before {
+ content: "\f5d2"; }
+
+.fa-audible:before {
+ content: "\f373"; }
+
+.fa-audio-description:before {
+ content: "\f29e"; }
+
+.fa-autoprefixer:before {
+ content: "\f41c"; }
+
+.fa-avianex:before {
+ content: "\f374"; }
+
+.fa-aviato:before {
+ content: "\f421"; }
+
+.fa-award:before {
+ content: "\f559"; }
+
+.fa-aws:before {
+ content: "\f375"; }
+
+.fa-baby:before {
+ content: "\f77c"; }
+
+.fa-baby-carriage:before {
+ content: "\f77d"; }
+
+.fa-backspace:before {
+ content: "\f55a"; }
+
+.fa-backward:before {
+ content: "\f04a"; }
+
+.fa-bacon:before {
+ content: "\f7e5"; }
+
+.fa-bacteria:before {
+ content: "\e059"; }
+
+.fa-bacterium:before {
+ content: "\e05a"; }
+
+.fa-bahai:before {
+ content: "\f666"; }
+
+.fa-balance-scale:before {
+ content: "\f24e"; }
+
+.fa-balance-scale-left:before {
+ content: "\f515"; }
+
+.fa-balance-scale-right:before {
+ content: "\f516"; }
+
+.fa-ban:before {
+ content: "\f05e"; }
+
+.fa-band-aid:before {
+ content: "\f462"; }
+
+.fa-bandcamp:before {
+ content: "\f2d5"; }
+
+.fa-barcode:before {
+ content: "\f02a"; }
+
+.fa-bars:before {
+ content: "\f0c9"; }
+
+.fa-baseball-ball:before {
+ content: "\f433"; }
+
+.fa-basketball-ball:before {
+ content: "\f434"; }
+
+.fa-bath:before {
+ content: "\f2cd"; }
+
+.fa-battery-empty:before {
+ content: "\f244"; }
+
+.fa-battery-full:before {
+ content: "\f240"; }
+
+.fa-battery-half:before {
+ content: "\f242"; }
+
+.fa-battery-quarter:before {
+ content: "\f243"; }
+
+.fa-battery-three-quarters:before {
+ content: "\f241"; }
+
+.fa-battle-net:before {
+ content: "\f835"; }
+
+.fa-bed:before {
+ content: "\f236"; }
+
+.fa-beer:before {
+ content: "\f0fc"; }
+
+.fa-behance:before {
+ content: "\f1b4"; }
+
+.fa-behance-square:before {
+ content: "\f1b5"; }
+
+.fa-bell:before {
+ content: "\f0f3"; }
+
+.fa-bell-slash:before {
+ content: "\f1f6"; }
+
+.fa-bezier-curve:before {
+ content: "\f55b"; }
+
+.fa-bible:before {
+ content: "\f647"; }
+
+.fa-bicycle:before {
+ content: "\f206"; }
+
+.fa-biking:before {
+ content: "\f84a"; }
+
+.fa-bimobject:before {
+ content: "\f378"; }
+
+.fa-binoculars:before {
+ content: "\f1e5"; }
+
+.fa-biohazard:before {
+ content: "\f780"; }
+
+.fa-birthday-cake:before {
+ content: "\f1fd"; }
+
+.fa-bitbucket:before {
+ content: "\f171"; }
+
+.fa-bitcoin:before {
+ content: "\f379"; }
+
+.fa-bity:before {
+ content: "\f37a"; }
+
+.fa-black-tie:before {
+ content: "\f27e"; }
+
+.fa-blackberry:before {
+ content: "\f37b"; }
+
+.fa-blender:before {
+ content: "\f517"; }
+
+.fa-blender-phone:before {
+ content: "\f6b6"; }
+
+.fa-blind:before {
+ content: "\f29d"; }
+
+.fa-blog:before {
+ content: "\f781"; }
+
+.fa-blogger:before {
+ content: "\f37c"; }
+
+.fa-blogger-b:before {
+ content: "\f37d"; }
+
+.fa-bluetooth:before {
+ content: "\f293"; }
+
+.fa-bluetooth-b:before {
+ content: "\f294"; }
+
+.fa-bold:before {
+ content: "\f032"; }
+
+.fa-bolt:before {
+ content: "\f0e7"; }
+
+.fa-bomb:before {
+ content: "\f1e2"; }
+
+.fa-bone:before {
+ content: "\f5d7"; }
+
+.fa-bong:before {
+ content: "\f55c"; }
+
+.fa-book:before {
+ content: "\f02d"; }
+
+.fa-book-dead:before {
+ content: "\f6b7"; }
+
+.fa-book-medical:before {
+ content: "\f7e6"; }
+
+.fa-book-open:before {
+ content: "\f518"; }
+
+.fa-book-reader:before {
+ content: "\f5da"; }
+
+.fa-bookmark:before {
+ content: "\f02e"; }
+
+.fa-bootstrap:before {
+ content: "\f836"; }
+
+.fa-border-all:before {
+ content: "\f84c"; }
+
+.fa-border-none:before {
+ content: "\f850"; }
+
+.fa-border-style:before {
+ content: "\f853"; }
+
+.fa-bowling-ball:before {
+ content: "\f436"; }
+
+.fa-box:before {
+ content: "\f466"; }
+
+.fa-box-open:before {
+ content: "\f49e"; }
+
+.fa-box-tissue:before {
+ content: "\e05b"; }
+
+.fa-boxes:before {
+ content: "\f468"; }
+
+.fa-braille:before {
+ content: "\f2a1"; }
+
+.fa-brain:before {
+ content: "\f5dc"; }
+
+.fa-bread-slice:before {
+ content: "\f7ec"; }
+
+.fa-briefcase:before {
+ content: "\f0b1"; }
+
+.fa-briefcase-medical:before {
+ content: "\f469"; }
+
+.fa-broadcast-tower:before {
+ content: "\f519"; }
+
+.fa-broom:before {
+ content: "\f51a"; }
+
+.fa-brush:before {
+ content: "\f55d"; }
+
+.fa-btc:before {
+ content: "\f15a"; }
+
+.fa-buffer:before {
+ content: "\f837"; }
+
+.fa-bug:before {
+ content: "\f188"; }
+
+.fa-building:before {
+ content: "\f1ad"; }
+
+.fa-bullhorn:before {
+ content: "\f0a1"; }
+
+.fa-bullseye:before {
+ content: "\f140"; }
+
+.fa-burn:before {
+ content: "\f46a"; }
+
+.fa-buromobelexperte:before {
+ content: "\f37f"; }
+
+.fa-bus:before {
+ content: "\f207"; }
+
+.fa-bus-alt:before {
+ content: "\f55e"; }
+
+.fa-business-time:before {
+ content: "\f64a"; }
+
+.fa-buy-n-large:before {
+ content: "\f8a6"; }
+
+.fa-buysellads:before {
+ content: "\f20d"; }
+
+.fa-calculator:before {
+ content: "\f1ec"; }
+
+.fa-calendar:before {
+ content: "\f133"; }
+
+.fa-calendar-alt:before {
+ content: "\f073"; }
+
+.fa-calendar-check:before {
+ content: "\f274"; }
+
+.fa-calendar-day:before {
+ content: "\f783"; }
+
+.fa-calendar-minus:before {
+ content: "\f272"; }
+
+.fa-calendar-plus:before {
+ content: "\f271"; }
+
+.fa-calendar-times:before {
+ content: "\f273"; }
+
+.fa-calendar-week:before {
+ content: "\f784"; }
+
+.fa-camera:before {
+ content: "\f030"; }
+
+.fa-camera-retro:before {
+ content: "\f083"; }
+
+.fa-campground:before {
+ content: "\f6bb"; }
+
+.fa-canadian-maple-leaf:before {
+ content: "\f785"; }
+
+.fa-candy-cane:before {
+ content: "\f786"; }
+
+.fa-cannabis:before {
+ content: "\f55f"; }
+
+.fa-capsules:before {
+ content: "\f46b"; }
+
+.fa-car:before {
+ content: "\f1b9"; }
+
+.fa-car-alt:before {
+ content: "\f5de"; }
+
+.fa-car-battery:before {
+ content: "\f5df"; }
+
+.fa-car-crash:before {
+ content: "\f5e1"; }
+
+.fa-car-side:before {
+ content: "\f5e4"; }
+
+.fa-caravan:before {
+ content: "\f8ff"; }
+
+.fa-caret-down:before {
+ content: "\f0d7"; }
+
+.fa-caret-left:before {
+ content: "\f0d9"; }
+
+.fa-caret-right:before {
+ content: "\f0da"; }
+
+.fa-caret-square-down:before {
+ content: "\f150"; }
+
+.fa-caret-square-left:before {
+ content: "\f191"; }
+
+.fa-caret-square-right:before {
+ content: "\f152"; }
+
+.fa-caret-square-up:before {
+ content: "\f151"; }
+
+.fa-caret-up:before {
+ content: "\f0d8"; }
+
+.fa-carrot:before {
+ content: "\f787"; }
+
+.fa-cart-arrow-down:before {
+ content: "\f218"; }
+
+.fa-cart-plus:before {
+ content: "\f217"; }
+
+.fa-cash-register:before {
+ content: "\f788"; }
+
+.fa-cat:before {
+ content: "\f6be"; }
+
+.fa-cc-amazon-pay:before {
+ content: "\f42d"; }
+
+.fa-cc-amex:before {
+ content: "\f1f3"; }
+
+.fa-cc-apple-pay:before {
+ content: "\f416"; }
+
+.fa-cc-diners-club:before {
+ content: "\f24c"; }
+
+.fa-cc-discover:before {
+ content: "\f1f2"; }
+
+.fa-cc-jcb:before {
+ content: "\f24b"; }
+
+.fa-cc-mastercard:before {
+ content: "\f1f1"; }
+
+.fa-cc-paypal:before {
+ content: "\f1f4"; }
+
+.fa-cc-stripe:before {
+ content: "\f1f5"; }
+
+.fa-cc-visa:before {
+ content: "\f1f0"; }
+
+.fa-centercode:before {
+ content: "\f380"; }
+
+.fa-centos:before {
+ content: "\f789"; }
+
+.fa-certificate:before {
+ content: "\f0a3"; }
+
+.fa-chair:before {
+ content: "\f6c0"; }
+
+.fa-chalkboard:before {
+ content: "\f51b"; }
+
+.fa-chalkboard-teacher:before {
+ content: "\f51c"; }
+
+.fa-charging-station:before {
+ content: "\f5e7"; }
+
+.fa-chart-area:before {
+ content: "\f1fe"; }
+
+.fa-chart-bar:before {
+ content: "\f080"; }
+
+.fa-chart-line:before {
+ content: "\f201"; }
+
+.fa-chart-pie:before {
+ content: "\f200"; }
+
+.fa-check:before {
+ content: "\f00c"; }
+
+.fa-check-circle:before {
+ content: "\f058"; }
+
+.fa-check-double:before {
+ content: "\f560"; }
+
+.fa-check-square:before {
+ content: "\f14a"; }
+
+.fa-cheese:before {
+ content: "\f7ef"; }
+
+.fa-chess:before {
+ content: "\f439"; }
+
+.fa-chess-bishop:before {
+ content: "\f43a"; }
+
+.fa-chess-board:before {
+ content: "\f43c"; }
+
+.fa-chess-king:before {
+ content: "\f43f"; }
+
+.fa-chess-knight:before {
+ content: "\f441"; }
+
+.fa-chess-pawn:before {
+ content: "\f443"; }
+
+.fa-chess-queen:before {
+ content: "\f445"; }
+
+.fa-chess-rook:before {
+ content: "\f447"; }
+
+.fa-chevron-circle-down:before {
+ content: "\f13a"; }
+
+.fa-chevron-circle-left:before {
+ content: "\f137"; }
+
+.fa-chevron-circle-right:before {
+ content: "\f138"; }
+
+.fa-chevron-circle-up:before {
+ content: "\f139"; }
+
+.fa-chevron-down:before {
+ content: "\f078"; }
+
+.fa-chevron-left:before {
+ content: "\f053"; }
+
+.fa-chevron-right:before {
+ content: "\f054"; }
+
+.fa-chevron-up:before {
+ content: "\f077"; }
+
+.fa-child:before {
+ content: "\f1ae"; }
+
+.fa-chrome:before {
+ content: "\f268"; }
+
+.fa-chromecast:before {
+ content: "\f838"; }
+
+.fa-church:before {
+ content: "\f51d"; }
+
+.fa-circle:before {
+ content: "\f111"; }
+
+.fa-circle-notch:before {
+ content: "\f1ce"; }
+
+.fa-city:before {
+ content: "\f64f"; }
+
+.fa-clinic-medical:before {
+ content: "\f7f2"; }
+
+.fa-clipboard:before {
+ content: "\f328"; }
+
+.fa-clipboard-check:before {
+ content: "\f46c"; }
+
+.fa-clipboard-list:before {
+ content: "\f46d"; }
+
+.fa-clock:before {
+ content: "\f017"; }
+
+.fa-clone:before {
+ content: "\f24d"; }
+
+.fa-closed-captioning:before {
+ content: "\f20a"; }
+
+.fa-cloud:before {
+ content: "\f0c2"; }
+
+.fa-cloud-download-alt:before {
+ content: "\f381"; }
+
+.fa-cloud-meatball:before {
+ content: "\f73b"; }
+
+.fa-cloud-moon:before {
+ content: "\f6c3"; }
+
+.fa-cloud-moon-rain:before {
+ content: "\f73c"; }
+
+.fa-cloud-rain:before {
+ content: "\f73d"; }
+
+.fa-cloud-showers-heavy:before {
+ content: "\f740"; }
+
+.fa-cloud-sun:before {
+ content: "\f6c4"; }
+
+.fa-cloud-sun-rain:before {
+ content: "\f743"; }
+
+.fa-cloud-upload-alt:before {
+ content: "\f382"; }
+
+.fa-cloudflare:before {
+ content: "\e07d"; }
+
+.fa-cloudscale:before {
+ content: "\f383"; }
+
+.fa-cloudsmith:before {
+ content: "\f384"; }
+
+.fa-cloudversify:before {
+ content: "\f385"; }
+
+.fa-cocktail:before {
+ content: "\f561"; }
+
+.fa-code:before {
+ content: "\f121"; }
+
+.fa-code-branch:before {
+ content: "\f126"; }
+
+.fa-codepen:before {
+ content: "\f1cb"; }
+
+.fa-codiepie:before {
+ content: "\f284"; }
+
+.fa-coffee:before {
+ content: "\f0f4"; }
+
+.fa-cog:before {
+ content: "\f013"; }
+
+.fa-cogs:before {
+ content: "\f085"; }
+
+.fa-coins:before {
+ content: "\f51e"; }
+
+.fa-columns:before {
+ content: "\f0db"; }
+
+.fa-comment:before {
+ content: "\f075"; }
+
+.fa-comment-alt:before {
+ content: "\f27a"; }
+
+.fa-comment-dollar:before {
+ content: "\f651"; }
+
+.fa-comment-dots:before {
+ content: "\f4ad"; }
+
+.fa-comment-medical:before {
+ content: "\f7f5"; }
+
+.fa-comment-slash:before {
+ content: "\f4b3"; }
+
+.fa-comments:before {
+ content: "\f086"; }
+
+.fa-comments-dollar:before {
+ content: "\f653"; }
+
+.fa-compact-disc:before {
+ content: "\f51f"; }
+
+.fa-compass:before {
+ content: "\f14e"; }
+
+.fa-compress:before {
+ content: "\f066"; }
+
+.fa-compress-alt:before {
+ content: "\f422"; }
+
+.fa-compress-arrows-alt:before {
+ content: "\f78c"; }
+
+.fa-concierge-bell:before {
+ content: "\f562"; }
+
+.fa-confluence:before {
+ content: "\f78d"; }
+
+.fa-connectdevelop:before {
+ content: "\f20e"; }
+
+.fa-contao:before {
+ content: "\f26d"; }
+
+.fa-cookie:before {
+ content: "\f563"; }
+
+.fa-cookie-bite:before {
+ content: "\f564"; }
+
+.fa-copy:before {
+ content: "\f0c5"; }
+
+.fa-copyright:before {
+ content: "\f1f9"; }
+
+.fa-cotton-bureau:before {
+ content: "\f89e"; }
+
+.fa-couch:before {
+ content: "\f4b8"; }
+
+.fa-cpanel:before {
+ content: "\f388"; }
+
+.fa-creative-commons:before {
+ content: "\f25e"; }
+
+.fa-creative-commons-by:before {
+ content: "\f4e7"; }
+
+.fa-creative-commons-nc:before {
+ content: "\f4e8"; }
+
+.fa-creative-commons-nc-eu:before {
+ content: "\f4e9"; }
+
+.fa-creative-commons-nc-jp:before {
+ content: "\f4ea"; }
+
+.fa-creative-commons-nd:before {
+ content: "\f4eb"; }
+
+.fa-creative-commons-pd:before {
+ content: "\f4ec"; }
+
+.fa-creative-commons-pd-alt:before {
+ content: "\f4ed"; }
+
+.fa-creative-commons-remix:before {
+ content: "\f4ee"; }
+
+.fa-creative-commons-sa:before {
+ content: "\f4ef"; }
+
+.fa-creative-commons-sampling:before {
+ content: "\f4f0"; }
+
+.fa-creative-commons-sampling-plus:before {
+ content: "\f4f1"; }
+
+.fa-creative-commons-share:before {
+ content: "\f4f2"; }
+
+.fa-creative-commons-zero:before {
+ content: "\f4f3"; }
+
+.fa-credit-card:before {
+ content: "\f09d"; }
+
+.fa-critical-role:before {
+ content: "\f6c9"; }
+
+.fa-crop:before {
+ content: "\f125"; }
+
+.fa-crop-alt:before {
+ content: "\f565"; }
+
+.fa-cross:before {
+ content: "\f654"; }
+
+.fa-crosshairs:before {
+ content: "\f05b"; }
+
+.fa-crow:before {
+ content: "\f520"; }
+
+.fa-crown:before {
+ content: "\f521"; }
+
+.fa-crutch:before {
+ content: "\f7f7"; }
+
+.fa-css3:before {
+ content: "\f13c"; }
+
+.fa-css3-alt:before {
+ content: "\f38b"; }
+
+.fa-cube:before {
+ content: "\f1b2"; }
+
+.fa-cubes:before {
+ content: "\f1b3"; }
+
+.fa-cut:before {
+ content: "\f0c4"; }
+
+.fa-cuttlefish:before {
+ content: "\f38c"; }
+
+.fa-d-and-d:before {
+ content: "\f38d"; }
+
+.fa-d-and-d-beyond:before {
+ content: "\f6ca"; }
+
+.fa-dailymotion:before {
+ content: "\e052"; }
+
+.fa-dashcube:before {
+ content: "\f210"; }
+
+.fa-database:before {
+ content: "\f1c0"; }
+
+.fa-deaf:before {
+ content: "\f2a4"; }
+
+.fa-deezer:before {
+ content: "\e077"; }
+
+.fa-delicious:before {
+ content: "\f1a5"; }
+
+.fa-democrat:before {
+ content: "\f747"; }
+
+.fa-deploydog:before {
+ content: "\f38e"; }
+
+.fa-deskpro:before {
+ content: "\f38f"; }
+
+.fa-desktop:before {
+ content: "\f108"; }
+
+.fa-dev:before {
+ content: "\f6cc"; }
+
+.fa-deviantart:before {
+ content: "\f1bd"; }
+
+.fa-dharmachakra:before {
+ content: "\f655"; }
+
+.fa-dhl:before {
+ content: "\f790"; }
+
+.fa-diagnoses:before {
+ content: "\f470"; }
+
+.fa-diaspora:before {
+ content: "\f791"; }
+
+.fa-dice:before {
+ content: "\f522"; }
+
+.fa-dice-d20:before {
+ content: "\f6cf"; }
+
+.fa-dice-d6:before {
+ content: "\f6d1"; }
+
+.fa-dice-five:before {
+ content: "\f523"; }
+
+.fa-dice-four:before {
+ content: "\f524"; }
+
+.fa-dice-one:before {
+ content: "\f525"; }
+
+.fa-dice-six:before {
+ content: "\f526"; }
+
+.fa-dice-three:before {
+ content: "\f527"; }
+
+.fa-dice-two:before {
+ content: "\f528"; }
+
+.fa-digg:before {
+ content: "\f1a6"; }
+
+.fa-digital-ocean:before {
+ content: "\f391"; }
+
+.fa-digital-tachograph:before {
+ content: "\f566"; }
+
+.fa-directions:before {
+ content: "\f5eb"; }
+
+.fa-discord:before {
+ content: "\f392"; }
+
+.fa-discourse:before {
+ content: "\f393"; }
+
+.fa-disease:before {
+ content: "\f7fa"; }
+
+.fa-divide:before {
+ content: "\f529"; }
+
+.fa-dizzy:before {
+ content: "\f567"; }
+
+.fa-dna:before {
+ content: "\f471"; }
+
+.fa-dochub:before {
+ content: "\f394"; }
+
+.fa-docker:before {
+ content: "\f395"; }
+
+.fa-dog:before {
+ content: "\f6d3"; }
+
+.fa-dollar-sign:before {
+ content: "\f155"; }
+
+.fa-dolly:before {
+ content: "\f472"; }
+
+.fa-dolly-flatbed:before {
+ content: "\f474"; }
+
+.fa-donate:before {
+ content: "\f4b9"; }
+
+.fa-door-closed:before {
+ content: "\f52a"; }
+
+.fa-door-open:before {
+ content: "\f52b"; }
+
+.fa-dot-circle:before {
+ content: "\f192"; }
+
+.fa-dove:before {
+ content: "\f4ba"; }
+
+.fa-download:before {
+ content: "\f019"; }
+
+.fa-draft2digital:before {
+ content: "\f396"; }
+
+.fa-drafting-compass:before {
+ content: "\f568"; }
+
+.fa-dragon:before {
+ content: "\f6d5"; }
+
+.fa-draw-polygon:before {
+ content: "\f5ee"; }
+
+.fa-dribbble:before {
+ content: "\f17d"; }
+
+.fa-dribbble-square:before {
+ content: "\f397"; }
+
+.fa-dropbox:before {
+ content: "\f16b"; }
+
+.fa-drum:before {
+ content: "\f569"; }
+
+.fa-drum-steelpan:before {
+ content: "\f56a"; }
+
+.fa-drumstick-bite:before {
+ content: "\f6d7"; }
+
+.fa-drupal:before {
+ content: "\f1a9"; }
+
+.fa-dumbbell:before {
+ content: "\f44b"; }
+
+.fa-dumpster:before {
+ content: "\f793"; }
+
+.fa-dumpster-fire:before {
+ content: "\f794"; }
+
+.fa-dungeon:before {
+ content: "\f6d9"; }
+
+.fa-dyalog:before {
+ content: "\f399"; }
+
+.fa-earlybirds:before {
+ content: "\f39a"; }
+
+.fa-ebay:before {
+ content: "\f4f4"; }
+
+.fa-edge:before {
+ content: "\f282"; }
+
+.fa-edge-legacy:before {
+ content: "\e078"; }
+
+.fa-edit:before {
+ content: "\f044"; }
+
+.fa-egg:before {
+ content: "\f7fb"; }
+
+.fa-eject:before {
+ content: "\f052"; }
+
+.fa-elementor:before {
+ content: "\f430"; }
+
+.fa-ellipsis-h:before {
+ content: "\f141"; }
+
+.fa-ellipsis-v:before {
+ content: "\f142"; }
+
+.fa-ello:before {
+ content: "\f5f1"; }
+
+.fa-ember:before {
+ content: "\f423"; }
+
+.fa-empire:before {
+ content: "\f1d1"; }
+
+.fa-envelope:before {
+ content: "\f0e0"; }
+
+.fa-envelope-open:before {
+ content: "\f2b6"; }
+
+.fa-envelope-open-text:before {
+ content: "\f658"; }
+
+.fa-envelope-square:before {
+ content: "\f199"; }
+
+.fa-envira:before {
+ content: "\f299"; }
+
+.fa-equals:before {
+ content: "\f52c"; }
+
+.fa-eraser:before {
+ content: "\f12d"; }
+
+.fa-erlang:before {
+ content: "\f39d"; }
+
+.fa-ethereum:before {
+ content: "\f42e"; }
+
+.fa-ethernet:before {
+ content: "\f796"; }
+
+.fa-etsy:before {
+ content: "\f2d7"; }
+
+.fa-euro-sign:before {
+ content: "\f153"; }
+
+.fa-evernote:before {
+ content: "\f839"; }
+
+.fa-exchange-alt:before {
+ content: "\f362"; }
+
+.fa-exclamation:before {
+ content: "\f12a"; }
+
+.fa-exclamation-circle:before {
+ content: "\f06a"; }
+
+.fa-exclamation-triangle:before {
+ content: "\f071"; }
+
+.fa-expand:before {
+ content: "\f065"; }
+
+.fa-expand-alt:before {
+ content: "\f424"; }
+
+.fa-expand-arrows-alt:before {
+ content: "\f31e"; }
+
+.fa-expeditedssl:before {
+ content: "\f23e"; }
+
+.fa-external-link-alt:before {
+ content: "\f35d"; }
+
+.fa-external-link-square-alt:before {
+ content: "\f360"; }
+
+.fa-eye:before {
+ content: "\f06e"; }
+
+.fa-eye-dropper:before {
+ content: "\f1fb"; }
+
+.fa-eye-slash:before {
+ content: "\f070"; }
+
+.fa-facebook:before {
+ content: "\f09a"; }
+
+.fa-facebook-f:before {
+ content: "\f39e"; }
+
+.fa-facebook-messenger:before {
+ content: "\f39f"; }
+
+.fa-facebook-square:before {
+ content: "\f082"; }
+
+.fa-fan:before {
+ content: "\f863"; }
+
+.fa-fantasy-flight-games:before {
+ content: "\f6dc"; }
+
+.fa-fast-backward:before {
+ content: "\f049"; }
+
+.fa-fast-forward:before {
+ content: "\f050"; }
+
+.fa-faucet:before {
+ content: "\e005"; }
+
+.fa-fax:before {
+ content: "\f1ac"; }
+
+.fa-feather:before {
+ content: "\f52d"; }
+
+.fa-feather-alt:before {
+ content: "\f56b"; }
+
+.fa-fedex:before {
+ content: "\f797"; }
+
+.fa-fedora:before {
+ content: "\f798"; }
+
+.fa-female:before {
+ content: "\f182"; }
+
+.fa-fighter-jet:before {
+ content: "\f0fb"; }
+
+.fa-figma:before {
+ content: "\f799"; }
+
+.fa-file:before {
+ content: "\f15b"; }
+
+.fa-file-alt:before {
+ content: "\f15c"; }
+
+.fa-file-archive:before {
+ content: "\f1c6"; }
+
+.fa-file-audio:before {
+ content: "\f1c7"; }
+
+.fa-file-code:before {
+ content: "\f1c9"; }
+
+.fa-file-contract:before {
+ content: "\f56c"; }
+
+.fa-file-csv:before {
+ content: "\f6dd"; }
+
+.fa-file-download:before {
+ content: "\f56d"; }
+
+.fa-file-excel:before {
+ content: "\f1c3"; }
+
+.fa-file-export:before {
+ content: "\f56e"; }
+
+.fa-file-image:before {
+ content: "\f1c5"; }
+
+.fa-file-import:before {
+ content: "\f56f"; }
+
+.fa-file-invoice:before {
+ content: "\f570"; }
+
+.fa-file-invoice-dollar:before {
+ content: "\f571"; }
+
+.fa-file-medical:before {
+ content: "\f477"; }
+
+.fa-file-medical-alt:before {
+ content: "\f478"; }
+
+.fa-file-pdf:before {
+ content: "\f1c1"; }
+
+.fa-file-powerpoint:before {
+ content: "\f1c4"; }
+
+.fa-file-prescription:before {
+ content: "\f572"; }
+
+.fa-file-signature:before {
+ content: "\f573"; }
+
+.fa-file-upload:before {
+ content: "\f574"; }
+
+.fa-file-video:before {
+ content: "\f1c8"; }
+
+.fa-file-word:before {
+ content: "\f1c2"; }
+
+.fa-fill:before {
+ content: "\f575"; }
+
+.fa-fill-drip:before {
+ content: "\f576"; }
+
+.fa-film:before {
+ content: "\f008"; }
+
+.fa-filter:before {
+ content: "\f0b0"; }
+
+.fa-fingerprint:before {
+ content: "\f577"; }
+
+.fa-fire:before {
+ content: "\f06d"; }
+
+.fa-fire-alt:before {
+ content: "\f7e4"; }
+
+.fa-fire-extinguisher:before {
+ content: "\f134"; }
+
+.fa-firefox:before {
+ content: "\f269"; }
+
+.fa-firefox-browser:before {
+ content: "\e007"; }
+
+.fa-first-aid:before {
+ content: "\f479"; }
+
+.fa-first-order:before {
+ content: "\f2b0"; }
+
+.fa-first-order-alt:before {
+ content: "\f50a"; }
+
+.fa-firstdraft:before {
+ content: "\f3a1"; }
+
+.fa-fish:before {
+ content: "\f578"; }
+
+.fa-fist-raised:before {
+ content: "\f6de"; }
+
+.fa-flag:before {
+ content: "\f024"; }
+
+.fa-flag-checkered:before {
+ content: "\f11e"; }
+
+.fa-flag-usa:before {
+ content: "\f74d"; }
+
+.fa-flask:before {
+ content: "\f0c3"; }
+
+.fa-flickr:before {
+ content: "\f16e"; }
+
+.fa-flipboard:before {
+ content: "\f44d"; }
+
+.fa-flushed:before {
+ content: "\f579"; }
+
+.fa-fly:before {
+ content: "\f417"; }
+
+.fa-folder:before {
+ content: "\f07b"; }
+
+.fa-folder-minus:before {
+ content: "\f65d"; }
+
+.fa-folder-open:before {
+ content: "\f07c"; }
+
+.fa-folder-plus:before {
+ content: "\f65e"; }
+
+.fa-font:before {
+ content: "\f031"; }
+
+.fa-font-awesome:before {
+ content: "\f2b4"; }
+
+.fa-font-awesome-alt:before {
+ content: "\f35c"; }
+
+.fa-font-awesome-flag:before {
+ content: "\f425"; }
+
+.fa-font-awesome-logo-full:before {
+ content: "\f4e6"; }
+
+.fa-fonticons:before {
+ content: "\f280"; }
+
+.fa-fonticons-fi:before {
+ content: "\f3a2"; }
+
+.fa-football-ball:before {
+ content: "\f44e"; }
+
+.fa-fort-awesome:before {
+ content: "\f286"; }
+
+.fa-fort-awesome-alt:before {
+ content: "\f3a3"; }
+
+.fa-forumbee:before {
+ content: "\f211"; }
+
+.fa-forward:before {
+ content: "\f04e"; }
+
+.fa-foursquare:before {
+ content: "\f180"; }
+
+.fa-free-code-camp:before {
+ content: "\f2c5"; }
+
+.fa-freebsd:before {
+ content: "\f3a4"; }
+
+.fa-frog:before {
+ content: "\f52e"; }
+
+.fa-frown:before {
+ content: "\f119"; }
+
+.fa-frown-open:before {
+ content: "\f57a"; }
+
+.fa-fulcrum:before {
+ content: "\f50b"; }
+
+.fa-funnel-dollar:before {
+ content: "\f662"; }
+
+.fa-futbol:before {
+ content: "\f1e3"; }
+
+.fa-galactic-republic:before {
+ content: "\f50c"; }
+
+.fa-galactic-senate:before {
+ content: "\f50d"; }
+
+.fa-gamepad:before {
+ content: "\f11b"; }
+
+.fa-gas-pump:before {
+ content: "\f52f"; }
+
+.fa-gavel:before {
+ content: "\f0e3"; }
+
+.fa-gem:before {
+ content: "\f3a5"; }
+
+.fa-genderless:before {
+ content: "\f22d"; }
+
+.fa-get-pocket:before {
+ content: "\f265"; }
+
+.fa-gg:before {
+ content: "\f260"; }
+
+.fa-gg-circle:before {
+ content: "\f261"; }
+
+.fa-ghost:before {
+ content: "\f6e2"; }
+
+.fa-gift:before {
+ content: "\f06b"; }
+
+.fa-gifts:before {
+ content: "\f79c"; }
+
+.fa-git:before {
+ content: "\f1d3"; }
+
+.fa-git-alt:before {
+ content: "\f841"; }
+
+.fa-git-square:before {
+ content: "\f1d2"; }
+
+.fa-github:before {
+ content: "\f09b"; }
+
+.fa-github-alt:before {
+ content: "\f113"; }
+
+.fa-github-square:before {
+ content: "\f092"; }
+
+.fa-gitkraken:before {
+ content: "\f3a6"; }
+
+.fa-gitlab:before {
+ content: "\f296"; }
+
+.fa-gitter:before {
+ content: "\f426"; }
+
+.fa-glass-cheers:before {
+ content: "\f79f"; }
+
+.fa-glass-martini:before {
+ content: "\f000"; }
+
+.fa-glass-martini-alt:before {
+ content: "\f57b"; }
+
+.fa-glass-whiskey:before {
+ content: "\f7a0"; }
+
+.fa-glasses:before {
+ content: "\f530"; }
+
+.fa-glide:before {
+ content: "\f2a5"; }
+
+.fa-glide-g:before {
+ content: "\f2a6"; }
+
+.fa-globe:before {
+ content: "\f0ac"; }
+
+.fa-globe-africa:before {
+ content: "\f57c"; }
+
+.fa-globe-americas:before {
+ content: "\f57d"; }
+
+.fa-globe-asia:before {
+ content: "\f57e"; }
+
+.fa-globe-europe:before {
+ content: "\f7a2"; }
+
+.fa-gofore:before {
+ content: "\f3a7"; }
+
+.fa-golf-ball:before {
+ content: "\f450"; }
+
+.fa-goodreads:before {
+ content: "\f3a8"; }
+
+.fa-goodreads-g:before {
+ content: "\f3a9"; }
+
+.fa-google:before {
+ content: "\f1a0"; }
+
+.fa-google-drive:before {
+ content: "\f3aa"; }
+
+.fa-google-pay:before {
+ content: "\e079"; }
+
+.fa-google-play:before {
+ content: "\f3ab"; }
+
+.fa-google-plus:before {
+ content: "\f2b3"; }
+
+.fa-google-plus-g:before {
+ content: "\f0d5"; }
+
+.fa-google-plus-square:before {
+ content: "\f0d4"; }
+
+.fa-google-wallet:before {
+ content: "\f1ee"; }
+
+.fa-gopuram:before {
+ content: "\f664"; }
+
+.fa-graduation-cap:before {
+ content: "\f19d"; }
+
+.fa-gratipay:before {
+ content: "\f184"; }
+
+.fa-grav:before {
+ content: "\f2d6"; }
+
+.fa-greater-than:before {
+ content: "\f531"; }
+
+.fa-greater-than-equal:before {
+ content: "\f532"; }
+
+.fa-grimace:before {
+ content: "\f57f"; }
+
+.fa-grin:before {
+ content: "\f580"; }
+
+.fa-grin-alt:before {
+ content: "\f581"; }
+
+.fa-grin-beam:before {
+ content: "\f582"; }
+
+.fa-grin-beam-sweat:before {
+ content: "\f583"; }
+
+.fa-grin-hearts:before {
+ content: "\f584"; }
+
+.fa-grin-squint:before {
+ content: "\f585"; }
+
+.fa-grin-squint-tears:before {
+ content: "\f586"; }
+
+.fa-grin-stars:before {
+ content: "\f587"; }
+
+.fa-grin-tears:before {
+ content: "\f588"; }
+
+.fa-grin-tongue:before {
+ content: "\f589"; }
+
+.fa-grin-tongue-squint:before {
+ content: "\f58a"; }
+
+.fa-grin-tongue-wink:before {
+ content: "\f58b"; }
+
+.fa-grin-wink:before {
+ content: "\f58c"; }
+
+.fa-grip-horizontal:before {
+ content: "\f58d"; }
+
+.fa-grip-lines:before {
+ content: "\f7a4"; }
+
+.fa-grip-lines-vertical:before {
+ content: "\f7a5"; }
+
+.fa-grip-vertical:before {
+ content: "\f58e"; }
+
+.fa-gripfire:before {
+ content: "\f3ac"; }
+
+.fa-grunt:before {
+ content: "\f3ad"; }
+
+.fa-guilded:before {
+ content: "\e07e"; }
+
+.fa-guitar:before {
+ content: "\f7a6"; }
+
+.fa-gulp:before {
+ content: "\f3ae"; }
+
+.fa-h-square:before {
+ content: "\f0fd"; }
+
+.fa-hacker-news:before {
+ content: "\f1d4"; }
+
+.fa-hacker-news-square:before {
+ content: "\f3af"; }
+
+.fa-hackerrank:before {
+ content: "\f5f7"; }
+
+.fa-hamburger:before {
+ content: "\f805"; }
+
+.fa-hammer:before {
+ content: "\f6e3"; }
+
+.fa-hamsa:before {
+ content: "\f665"; }
+
+.fa-hand-holding:before {
+ content: "\f4bd"; }
+
+.fa-hand-holding-heart:before {
+ content: "\f4be"; }
+
+.fa-hand-holding-medical:before {
+ content: "\e05c"; }
+
+.fa-hand-holding-usd:before {
+ content: "\f4c0"; }
+
+.fa-hand-holding-water:before {
+ content: "\f4c1"; }
+
+.fa-hand-lizard:before {
+ content: "\f258"; }
+
+.fa-hand-middle-finger:before {
+ content: "\f806"; }
+
+.fa-hand-paper:before {
+ content: "\f256"; }
+
+.fa-hand-peace:before {
+ content: "\f25b"; }
+
+.fa-hand-point-down:before {
+ content: "\f0a7"; }
+
+.fa-hand-point-left:before {
+ content: "\f0a5"; }
+
+.fa-hand-point-right:before {
+ content: "\f0a4"; }
+
+.fa-hand-point-up:before {
+ content: "\f0a6"; }
+
+.fa-hand-pointer:before {
+ content: "\f25a"; }
+
+.fa-hand-rock:before {
+ content: "\f255"; }
+
+.fa-hand-scissors:before {
+ content: "\f257"; }
+
+.fa-hand-sparkles:before {
+ content: "\e05d"; }
+
+.fa-hand-spock:before {
+ content: "\f259"; }
+
+.fa-hands:before {
+ content: "\f4c2"; }
+
+.fa-hands-helping:before {
+ content: "\f4c4"; }
+
+.fa-hands-wash:before {
+ content: "\e05e"; }
+
+.fa-handshake:before {
+ content: "\f2b5"; }
+
+.fa-handshake-alt-slash:before {
+ content: "\e05f"; }
+
+.fa-handshake-slash:before {
+ content: "\e060"; }
+
+.fa-hanukiah:before {
+ content: "\f6e6"; }
+
+.fa-hard-hat:before {
+ content: "\f807"; }
+
+.fa-hashtag:before {
+ content: "\f292"; }
+
+.fa-hat-cowboy:before {
+ content: "\f8c0"; }
+
+.fa-hat-cowboy-side:before {
+ content: "\f8c1"; }
+
+.fa-hat-wizard:before {
+ content: "\f6e8"; }
+
+.fa-hdd:before {
+ content: "\f0a0"; }
+
+.fa-head-side-cough:before {
+ content: "\e061"; }
+
+.fa-head-side-cough-slash:before {
+ content: "\e062"; }
+
+.fa-head-side-mask:before {
+ content: "\e063"; }
+
+.fa-head-side-virus:before {
+ content: "\e064"; }
+
+.fa-heading:before {
+ content: "\f1dc"; }
+
+.fa-headphones:before {
+ content: "\f025"; }
+
+.fa-headphones-alt:before {
+ content: "\f58f"; }
+
+.fa-headset:before {
+ content: "\f590"; }
+
+.fa-heart:before {
+ content: "\f004"; }
+
+.fa-heart-broken:before {
+ content: "\f7a9"; }
+
+.fa-heartbeat:before {
+ content: "\f21e"; }
+
+.fa-helicopter:before {
+ content: "\f533"; }
+
+.fa-highlighter:before {
+ content: "\f591"; }
+
+.fa-hiking:before {
+ content: "\f6ec"; }
+
+.fa-hippo:before {
+ content: "\f6ed"; }
+
+.fa-hips:before {
+ content: "\f452"; }
+
+.fa-hire-a-helper:before {
+ content: "\f3b0"; }
+
+.fa-history:before {
+ content: "\f1da"; }
+
+.fa-hive:before {
+ content: "\e07f"; }
+
+.fa-hockey-puck:before {
+ content: "\f453"; }
+
+.fa-holly-berry:before {
+ content: "\f7aa"; }
+
+.fa-home:before {
+ content: "\f015"; }
+
+.fa-hooli:before {
+ content: "\f427"; }
+
+.fa-hornbill:before {
+ content: "\f592"; }
+
+.fa-horse:before {
+ content: "\f6f0"; }
+
+.fa-horse-head:before {
+ content: "\f7ab"; }
+
+.fa-hospital:before {
+ content: "\f0f8"; }
+
+.fa-hospital-alt:before {
+ content: "\f47d"; }
+
+.fa-hospital-symbol:before {
+ content: "\f47e"; }
+
+.fa-hospital-user:before {
+ content: "\f80d"; }
+
+.fa-hot-tub:before {
+ content: "\f593"; }
+
+.fa-hotdog:before {
+ content: "\f80f"; }
+
+.fa-hotel:before {
+ content: "\f594"; }
+
+.fa-hotjar:before {
+ content: "\f3b1"; }
+
+.fa-hourglass:before {
+ content: "\f254"; }
+
+.fa-hourglass-end:before {
+ content: "\f253"; }
+
+.fa-hourglass-half:before {
+ content: "\f252"; }
+
+.fa-hourglass-start:before {
+ content: "\f251"; }
+
+.fa-house-damage:before {
+ content: "\f6f1"; }
+
+.fa-house-user:before {
+ content: "\e065"; }
+
+.fa-houzz:before {
+ content: "\f27c"; }
+
+.fa-hryvnia:before {
+ content: "\f6f2"; }
+
+.fa-html5:before {
+ content: "\f13b"; }
+
+.fa-hubspot:before {
+ content: "\f3b2"; }
+
+.fa-i-cursor:before {
+ content: "\f246"; }
+
+.fa-ice-cream:before {
+ content: "\f810"; }
+
+.fa-icicles:before {
+ content: "\f7ad"; }
+
+.fa-icons:before {
+ content: "\f86d"; }
+
+.fa-id-badge:before {
+ content: "\f2c1"; }
+
+.fa-id-card:before {
+ content: "\f2c2"; }
+
+.fa-id-card-alt:before {
+ content: "\f47f"; }
+
+.fa-ideal:before {
+ content: "\e013"; }
+
+.fa-igloo:before {
+ content: "\f7ae"; }
+
+.fa-image:before {
+ content: "\f03e"; }
+
+.fa-images:before {
+ content: "\f302"; }
+
+.fa-imdb:before {
+ content: "\f2d8"; }
+
+.fa-inbox:before {
+ content: "\f01c"; }
+
+.fa-indent:before {
+ content: "\f03c"; }
+
+.fa-industry:before {
+ content: "\f275"; }
+
+.fa-infinity:before {
+ content: "\f534"; }
+
+.fa-info:before {
+ content: "\f129"; }
+
+.fa-info-circle:before {
+ content: "\f05a"; }
+
+.fa-innosoft:before {
+ content: "\e080"; }
+
+.fa-instagram:before {
+ content: "\f16d"; }
+
+.fa-instagram-square:before {
+ content: "\e055"; }
+
+.fa-instalod:before {
+ content: "\e081"; }
+
+.fa-intercom:before {
+ content: "\f7af"; }
+
+.fa-internet-explorer:before {
+ content: "\f26b"; }
+
+.fa-invision:before {
+ content: "\f7b0"; }
+
+.fa-ioxhost:before {
+ content: "\f208"; }
+
+.fa-italic:before {
+ content: "\f033"; }
+
+.fa-itch-io:before {
+ content: "\f83a"; }
+
+.fa-itunes:before {
+ content: "\f3b4"; }
+
+.fa-itunes-note:before {
+ content: "\f3b5"; }
+
+.fa-java:before {
+ content: "\f4e4"; }
+
+.fa-jedi:before {
+ content: "\f669"; }
+
+.fa-jedi-order:before {
+ content: "\f50e"; }
+
+.fa-jenkins:before {
+ content: "\f3b6"; }
+
+.fa-jira:before {
+ content: "\f7b1"; }
+
+.fa-joget:before {
+ content: "\f3b7"; }
+
+.fa-joint:before {
+ content: "\f595"; }
+
+.fa-joomla:before {
+ content: "\f1aa"; }
+
+.fa-journal-whills:before {
+ content: "\f66a"; }
+
+.fa-js:before {
+ content: "\f3b8"; }
+
+.fa-js-square:before {
+ content: "\f3b9"; }
+
+.fa-jsfiddle:before {
+ content: "\f1cc"; }
+
+.fa-kaaba:before {
+ content: "\f66b"; }
+
+.fa-kaggle:before {
+ content: "\f5fa"; }
+
+.fa-key:before {
+ content: "\f084"; }
+
+.fa-keybase:before {
+ content: "\f4f5"; }
+
+.fa-keyboard:before {
+ content: "\f11c"; }
+
+.fa-keycdn:before {
+ content: "\f3ba"; }
+
+.fa-khanda:before {
+ content: "\f66d"; }
+
+.fa-kickstarter:before {
+ content: "\f3bb"; }
+
+.fa-kickstarter-k:before {
+ content: "\f3bc"; }
+
+.fa-kiss:before {
+ content: "\f596"; }
+
+.fa-kiss-beam:before {
+ content: "\f597"; }
+
+.fa-kiss-wink-heart:before {
+ content: "\f598"; }
+
+.fa-kiwi-bird:before {
+ content: "\f535"; }
+
+.fa-korvue:before {
+ content: "\f42f"; }
+
+.fa-landmark:before {
+ content: "\f66f"; }
+
+.fa-language:before {
+ content: "\f1ab"; }
+
+.fa-laptop:before {
+ content: "\f109"; }
+
+.fa-laptop-code:before {
+ content: "\f5fc"; }
+
+.fa-laptop-house:before {
+ content: "\e066"; }
+
+.fa-laptop-medical:before {
+ content: "\f812"; }
+
+.fa-laravel:before {
+ content: "\f3bd"; }
+
+.fa-lastfm:before {
+ content: "\f202"; }
+
+.fa-lastfm-square:before {
+ content: "\f203"; }
+
+.fa-laugh:before {
+ content: "\f599"; }
+
+.fa-laugh-beam:before {
+ content: "\f59a"; }
+
+.fa-laugh-squint:before {
+ content: "\f59b"; }
+
+.fa-laugh-wink:before {
+ content: "\f59c"; }
+
+.fa-layer-group:before {
+ content: "\f5fd"; }
+
+.fa-leaf:before {
+ content: "\f06c"; }
+
+.fa-leanpub:before {
+ content: "\f212"; }
+
+.fa-lemon:before {
+ content: "\f094"; }
+
+.fa-less:before {
+ content: "\f41d"; }
+
+.fa-less-than:before {
+ content: "\f536"; }
+
+.fa-less-than-equal:before {
+ content: "\f537"; }
+
+.fa-level-down-alt:before {
+ content: "\f3be"; }
+
+.fa-level-up-alt:before {
+ content: "\f3bf"; }
+
+.fa-life-ring:before {
+ content: "\f1cd"; }
+
+.fa-lightbulb:before {
+ content: "\f0eb"; }
+
+.fa-line:before {
+ content: "\f3c0"; }
+
+.fa-link:before {
+ content: "\f0c1"; }
+
+.fa-linkedin:before {
+ content: "\f08c"; }
+
+.fa-linkedin-in:before {
+ content: "\f0e1"; }
+
+.fa-linode:before {
+ content: "\f2b8"; }
+
+.fa-linux:before {
+ content: "\f17c"; }
+
+.fa-lira-sign:before {
+ content: "\f195"; }
+
+.fa-list:before {
+ content: "\f03a"; }
+
+.fa-list-alt:before {
+ content: "\f022"; }
+
+.fa-list-ol:before {
+ content: "\f0cb"; }
+
+.fa-list-ul:before {
+ content: "\f0ca"; }
+
+.fa-location-arrow:before {
+ content: "\f124"; }
+
+.fa-lock:before {
+ content: "\f023"; }
+
+.fa-lock-open:before {
+ content: "\f3c1"; }
+
+.fa-long-arrow-alt-down:before {
+ content: "\f309"; }
+
+.fa-long-arrow-alt-left:before {
+ content: "\f30a"; }
+
+.fa-long-arrow-alt-right:before {
+ content: "\f30b"; }
+
+.fa-long-arrow-alt-up:before {
+ content: "\f30c"; }
+
+.fa-low-vision:before {
+ content: "\f2a8"; }
+
+.fa-luggage-cart:before {
+ content: "\f59d"; }
+
+.fa-lungs:before {
+ content: "\f604"; }
+
+.fa-lungs-virus:before {
+ content: "\e067"; }
+
+.fa-lyft:before {
+ content: "\f3c3"; }
+
+.fa-magento:before {
+ content: "\f3c4"; }
+
+.fa-magic:before {
+ content: "\f0d0"; }
+
+.fa-magnet:before {
+ content: "\f076"; }
+
+.fa-mail-bulk:before {
+ content: "\f674"; }
+
+.fa-mailchimp:before {
+ content: "\f59e"; }
+
+.fa-male:before {
+ content: "\f183"; }
+
+.fa-mandalorian:before {
+ content: "\f50f"; }
+
+.fa-map:before {
+ content: "\f279"; }
+
+.fa-map-marked:before {
+ content: "\f59f"; }
+
+.fa-map-marked-alt:before {
+ content: "\f5a0"; }
+
+.fa-map-marker:before {
+ content: "\f041"; }
+
+.fa-map-marker-alt:before {
+ content: "\f3c5"; }
+
+.fa-map-pin:before {
+ content: "\f276"; }
+
+.fa-map-signs:before {
+ content: "\f277"; }
+
+.fa-markdown:before {
+ content: "\f60f"; }
+
+.fa-marker:before {
+ content: "\f5a1"; }
+
+.fa-mars:before {
+ content: "\f222"; }
+
+.fa-mars-double:before {
+ content: "\f227"; }
+
+.fa-mars-stroke:before {
+ content: "\f229"; }
+
+.fa-mars-stroke-h:before {
+ content: "\f22b"; }
+
+.fa-mars-stroke-v:before {
+ content: "\f22a"; }
+
+.fa-mask:before {
+ content: "\f6fa"; }
+
+.fa-mastodon:before {
+ content: "\f4f6"; }
+
+.fa-maxcdn:before {
+ content: "\f136"; }
+
+.fa-mdb:before {
+ content: "\f8ca"; }
+
+.fa-medal:before {
+ content: "\f5a2"; }
+
+.fa-medapps:before {
+ content: "\f3c6"; }
+
+.fa-medium:before {
+ content: "\f23a"; }
+
+.fa-medium-m:before {
+ content: "\f3c7"; }
+
+.fa-medkit:before {
+ content: "\f0fa"; }
+
+.fa-medrt:before {
+ content: "\f3c8"; }
+
+.fa-meetup:before {
+ content: "\f2e0"; }
+
+.fa-megaport:before {
+ content: "\f5a3"; }
+
+.fa-meh:before {
+ content: "\f11a"; }
+
+.fa-meh-blank:before {
+ content: "\f5a4"; }
+
+.fa-meh-rolling-eyes:before {
+ content: "\f5a5"; }
+
+.fa-memory:before {
+ content: "\f538"; }
+
+.fa-mendeley:before {
+ content: "\f7b3"; }
+
+.fa-menorah:before {
+ content: "\f676"; }
+
+.fa-mercury:before {
+ content: "\f223"; }
+
+.fa-meteor:before {
+ content: "\f753"; }
+
+.fa-microblog:before {
+ content: "\e01a"; }
+
+.fa-microchip:before {
+ content: "\f2db"; }
+
+.fa-microphone:before {
+ content: "\f130"; }
+
+.fa-microphone-alt:before {
+ content: "\f3c9"; }
+
+.fa-microphone-alt-slash:before {
+ content: "\f539"; }
+
+.fa-microphone-slash:before {
+ content: "\f131"; }
+
+.fa-microscope:before {
+ content: "\f610"; }
+
+.fa-microsoft:before {
+ content: "\f3ca"; }
+
+.fa-minus:before {
+ content: "\f068"; }
+
+.fa-minus-circle:before {
+ content: "\f056"; }
+
+.fa-minus-square:before {
+ content: "\f146"; }
+
+.fa-mitten:before {
+ content: "\f7b5"; }
+
+.fa-mix:before {
+ content: "\f3cb"; }
+
+.fa-mixcloud:before {
+ content: "\f289"; }
+
+.fa-mixer:before {
+ content: "\e056"; }
+
+.fa-mizuni:before {
+ content: "\f3cc"; }
+
+.fa-mobile:before {
+ content: "\f10b"; }
+
+.fa-mobile-alt:before {
+ content: "\f3cd"; }
+
+.fa-modx:before {
+ content: "\f285"; }
+
+.fa-monero:before {
+ content: "\f3d0"; }
+
+.fa-money-bill:before {
+ content: "\f0d6"; }
+
+.fa-money-bill-alt:before {
+ content: "\f3d1"; }
+
+.fa-money-bill-wave:before {
+ content: "\f53a"; }
+
+.fa-money-bill-wave-alt:before {
+ content: "\f53b"; }
+
+.fa-money-check:before {
+ content: "\f53c"; }
+
+.fa-money-check-alt:before {
+ content: "\f53d"; }
+
+.fa-monument:before {
+ content: "\f5a6"; }
+
+.fa-moon:before {
+ content: "\f186"; }
+
+.fa-mortar-pestle:before {
+ content: "\f5a7"; }
+
+.fa-mosque:before {
+ content: "\f678"; }
+
+.fa-motorcycle:before {
+ content: "\f21c"; }
+
+.fa-mountain:before {
+ content: "\f6fc"; }
+
+.fa-mouse:before {
+ content: "\f8cc"; }
+
+.fa-mouse-pointer:before {
+ content: "\f245"; }
+
+.fa-mug-hot:before {
+ content: "\f7b6"; }
+
+.fa-music:before {
+ content: "\f001"; }
+
+.fa-napster:before {
+ content: "\f3d2"; }
+
+.fa-neos:before {
+ content: "\f612"; }
+
+.fa-network-wired:before {
+ content: "\f6ff"; }
+
+.fa-neuter:before {
+ content: "\f22c"; }
+
+.fa-newspaper:before {
+ content: "\f1ea"; }
+
+.fa-nimblr:before {
+ content: "\f5a8"; }
+
+.fa-node:before {
+ content: "\f419"; }
+
+.fa-node-js:before {
+ content: "\f3d3"; }
+
+.fa-not-equal:before {
+ content: "\f53e"; }
+
+.fa-notes-medical:before {
+ content: "\f481"; }
+
+.fa-npm:before {
+ content: "\f3d4"; }
+
+.fa-ns8:before {
+ content: "\f3d5"; }
+
+.fa-nutritionix:before {
+ content: "\f3d6"; }
+
+.fa-object-group:before {
+ content: "\f247"; }
+
+.fa-object-ungroup:before {
+ content: "\f248"; }
+
+.fa-octopus-deploy:before {
+ content: "\e082"; }
+
+.fa-odnoklassniki:before {
+ content: "\f263"; }
+
+.fa-odnoklassniki-square:before {
+ content: "\f264"; }
+
+.fa-oil-can:before {
+ content: "\f613"; }
+
+.fa-old-republic:before {
+ content: "\f510"; }
+
+.fa-om:before {
+ content: "\f679"; }
+
+.fa-opencart:before {
+ content: "\f23d"; }
+
+.fa-openid:before {
+ content: "\f19b"; }
+
+.fa-opera:before {
+ content: "\f26a"; }
+
+.fa-optin-monster:before {
+ content: "\f23c"; }
+
+.fa-orcid:before {
+ content: "\f8d2"; }
+
+.fa-osi:before {
+ content: "\f41a"; }
+
+.fa-otter:before {
+ content: "\f700"; }
+
+.fa-outdent:before {
+ content: "\f03b"; }
+
+.fa-page4:before {
+ content: "\f3d7"; }
+
+.fa-pagelines:before {
+ content: "\f18c"; }
+
+.fa-pager:before {
+ content: "\f815"; }
+
+.fa-paint-brush:before {
+ content: "\f1fc"; }
+
+.fa-paint-roller:before {
+ content: "\f5aa"; }
+
+.fa-palette:before {
+ content: "\f53f"; }
+
+.fa-palfed:before {
+ content: "\f3d8"; }
+
+.fa-pallet:before {
+ content: "\f482"; }
+
+.fa-paper-plane:before {
+ content: "\f1d8"; }
+
+.fa-paperclip:before {
+ content: "\f0c6"; }
+
+.fa-parachute-box:before {
+ content: "\f4cd"; }
+
+.fa-paragraph:before {
+ content: "\f1dd"; }
+
+.fa-parking:before {
+ content: "\f540"; }
+
+.fa-passport:before {
+ content: "\f5ab"; }
+
+.fa-pastafarianism:before {
+ content: "\f67b"; }
+
+.fa-paste:before {
+ content: "\f0ea"; }
+
+.fa-patreon:before {
+ content: "\f3d9"; }
+
+.fa-pause:before {
+ content: "\f04c"; }
+
+.fa-pause-circle:before {
+ content: "\f28b"; }
+
+.fa-paw:before {
+ content: "\f1b0"; }
+
+.fa-paypal:before {
+ content: "\f1ed"; }
+
+.fa-peace:before {
+ content: "\f67c"; }
+
+.fa-pen:before {
+ content: "\f304"; }
+
+.fa-pen-alt:before {
+ content: "\f305"; }
+
+.fa-pen-fancy:before {
+ content: "\f5ac"; }
+
+.fa-pen-nib:before {
+ content: "\f5ad"; }
+
+.fa-pen-square:before {
+ content: "\f14b"; }
+
+.fa-pencil-alt:before {
+ content: "\f303"; }
+
+.fa-pencil-ruler:before {
+ content: "\f5ae"; }
+
+.fa-penny-arcade:before {
+ content: "\f704"; }
+
+.fa-people-arrows:before {
+ content: "\e068"; }
+
+.fa-people-carry:before {
+ content: "\f4ce"; }
+
+.fa-pepper-hot:before {
+ content: "\f816"; }
+
+.fa-perbyte:before {
+ content: "\e083"; }
+
+.fa-percent:before {
+ content: "\f295"; }
+
+.fa-percentage:before {
+ content: "\f541"; }
+
+.fa-periscope:before {
+ content: "\f3da"; }
+
+.fa-person-booth:before {
+ content: "\f756"; }
+
+.fa-phabricator:before {
+ content: "\f3db"; }
+
+.fa-phoenix-framework:before {
+ content: "\f3dc"; }
+
+.fa-phoenix-squadron:before {
+ content: "\f511"; }
+
+.fa-phone:before {
+ content: "\f095"; }
+
+.fa-phone-alt:before {
+ content: "\f879"; }
+
+.fa-phone-slash:before {
+ content: "\f3dd"; }
+
+.fa-phone-square:before {
+ content: "\f098"; }
+
+.fa-phone-square-alt:before {
+ content: "\f87b"; }
+
+.fa-phone-volume:before {
+ content: "\f2a0"; }
+
+.fa-photo-video:before {
+ content: "\f87c"; }
+
+.fa-php:before {
+ content: "\f457"; }
+
+.fa-pied-piper:before {
+ content: "\f2ae"; }
+
+.fa-pied-piper-alt:before {
+ content: "\f1a8"; }
+
+.fa-pied-piper-hat:before {
+ content: "\f4e5"; }
+
+.fa-pied-piper-pp:before {
+ content: "\f1a7"; }
+
+.fa-pied-piper-square:before {
+ content: "\e01e"; }
+
+.fa-piggy-bank:before {
+ content: "\f4d3"; }
+
+.fa-pills:before {
+ content: "\f484"; }
+
+.fa-pinterest:before {
+ content: "\f0d2"; }
+
+.fa-pinterest-p:before {
+ content: "\f231"; }
+
+.fa-pinterest-square:before {
+ content: "\f0d3"; }
+
+.fa-pizza-slice:before {
+ content: "\f818"; }
+
+.fa-place-of-worship:before {
+ content: "\f67f"; }
+
+.fa-plane:before {
+ content: "\f072"; }
+
+.fa-plane-arrival:before {
+ content: "\f5af"; }
+
+.fa-plane-departure:before {
+ content: "\f5b0"; }
+
+.fa-plane-slash:before {
+ content: "\e069"; }
+
+.fa-play:before {
+ content: "\f04b"; }
+
+.fa-play-circle:before {
+ content: "\f144"; }
+
+.fa-playstation:before {
+ content: "\f3df"; }
+
+.fa-plug:before {
+ content: "\f1e6"; }
+
+.fa-plus:before {
+ content: "\f067"; }
+
+.fa-plus-circle:before {
+ content: "\f055"; }
+
+.fa-plus-square:before {
+ content: "\f0fe"; }
+
+.fa-podcast:before {
+ content: "\f2ce"; }
+
+.fa-poll:before {
+ content: "\f681"; }
+
+.fa-poll-h:before {
+ content: "\f682"; }
+
+.fa-poo:before {
+ content: "\f2fe"; }
+
+.fa-poo-storm:before {
+ content: "\f75a"; }
+
+.fa-poop:before {
+ content: "\f619"; }
+
+.fa-portrait:before {
+ content: "\f3e0"; }
+
+.fa-pound-sign:before {
+ content: "\f154"; }
+
+.fa-power-off:before {
+ content: "\f011"; }
+
+.fa-pray:before {
+ content: "\f683"; }
+
+.fa-praying-hands:before {
+ content: "\f684"; }
+
+.fa-prescription:before {
+ content: "\f5b1"; }
+
+.fa-prescription-bottle:before {
+ content: "\f485"; }
+
+.fa-prescription-bottle-alt:before {
+ content: "\f486"; }
+
+.fa-print:before {
+ content: "\f02f"; }
+
+.fa-procedures:before {
+ content: "\f487"; }
+
+.fa-product-hunt:before {
+ content: "\f288"; }
+
+.fa-project-diagram:before {
+ content: "\f542"; }
+
+.fa-pump-medical:before {
+ content: "\e06a"; }
+
+.fa-pump-soap:before {
+ content: "\e06b"; }
+
+.fa-pushed:before {
+ content: "\f3e1"; }
+
+.fa-puzzle-piece:before {
+ content: "\f12e"; }
+
+.fa-python:before {
+ content: "\f3e2"; }
+
+.fa-qq:before {
+ content: "\f1d6"; }
+
+.fa-qrcode:before {
+ content: "\f029"; }
+
+.fa-question:before {
+ content: "\f128"; }
+
+.fa-question-circle:before {
+ content: "\f059"; }
+
+.fa-quidditch:before {
+ content: "\f458"; }
+
+.fa-quinscape:before {
+ content: "\f459"; }
+
+.fa-quora:before {
+ content: "\f2c4"; }
+
+.fa-quote-left:before {
+ content: "\f10d"; }
+
+.fa-quote-right:before {
+ content: "\f10e"; }
+
+.fa-quran:before {
+ content: "\f687"; }
+
+.fa-r-project:before {
+ content: "\f4f7"; }
+
+.fa-radiation:before {
+ content: "\f7b9"; }
+
+.fa-radiation-alt:before {
+ content: "\f7ba"; }
+
+.fa-rainbow:before {
+ content: "\f75b"; }
+
+.fa-random:before {
+ content: "\f074"; }
+
+.fa-raspberry-pi:before {
+ content: "\f7bb"; }
+
+.fa-ravelry:before {
+ content: "\f2d9"; }
+
+.fa-react:before {
+ content: "\f41b"; }
+
+.fa-reacteurope:before {
+ content: "\f75d"; }
+
+.fa-readme:before {
+ content: "\f4d5"; }
+
+.fa-rebel:before {
+ content: "\f1d0"; }
+
+.fa-receipt:before {
+ content: "\f543"; }
+
+.fa-record-vinyl:before {
+ content: "\f8d9"; }
+
+.fa-recycle:before {
+ content: "\f1b8"; }
+
+.fa-red-river:before {
+ content: "\f3e3"; }
+
+.fa-reddit:before {
+ content: "\f1a1"; }
+
+.fa-reddit-alien:before {
+ content: "\f281"; }
+
+.fa-reddit-square:before {
+ content: "\f1a2"; }
+
+.fa-redhat:before {
+ content: "\f7bc"; }
+
+.fa-redo:before {
+ content: "\f01e"; }
+
+.fa-redo-alt:before {
+ content: "\f2f9"; }
+
+.fa-registered:before {
+ content: "\f25d"; }
+
+.fa-remove-format:before {
+ content: "\f87d"; }
+
+.fa-renren:before {
+ content: "\f18b"; }
+
+.fa-reply:before {
+ content: "\f3e5"; }
+
+.fa-reply-all:before {
+ content: "\f122"; }
+
+.fa-replyd:before {
+ content: "\f3e6"; }
+
+.fa-republican:before {
+ content: "\f75e"; }
+
+.fa-researchgate:before {
+ content: "\f4f8"; }
+
+.fa-resolving:before {
+ content: "\f3e7"; }
+
+.fa-restroom:before {
+ content: "\f7bd"; }
+
+.fa-retweet:before {
+ content: "\f079"; }
+
+.fa-rev:before {
+ content: "\f5b2"; }
+
+.fa-ribbon:before {
+ content: "\f4d6"; }
+
+.fa-ring:before {
+ content: "\f70b"; }
+
+.fa-road:before {
+ content: "\f018"; }
+
+.fa-robot:before {
+ content: "\f544"; }
+
+.fa-rocket:before {
+ content: "\f135"; }
+
+.fa-rocketchat:before {
+ content: "\f3e8"; }
+
+.fa-rockrms:before {
+ content: "\f3e9"; }
+
+.fa-route:before {
+ content: "\f4d7"; }
+
+.fa-rss:before {
+ content: "\f09e"; }
+
+.fa-rss-square:before {
+ content: "\f143"; }
+
+.fa-ruble-sign:before {
+ content: "\f158"; }
+
+.fa-ruler:before {
+ content: "\f545"; }
+
+.fa-ruler-combined:before {
+ content: "\f546"; }
+
+.fa-ruler-horizontal:before {
+ content: "\f547"; }
+
+.fa-ruler-vertical:before {
+ content: "\f548"; }
+
+.fa-running:before {
+ content: "\f70c"; }
+
+.fa-rupee-sign:before {
+ content: "\f156"; }
+
+.fa-rust:before {
+ content: "\e07a"; }
+
+.fa-sad-cry:before {
+ content: "\f5b3"; }
+
+.fa-sad-tear:before {
+ content: "\f5b4"; }
+
+.fa-safari:before {
+ content: "\f267"; }
+
+.fa-salesforce:before {
+ content: "\f83b"; }
+
+.fa-sass:before {
+ content: "\f41e"; }
+
+.fa-satellite:before {
+ content: "\f7bf"; }
+
+.fa-satellite-dish:before {
+ content: "\f7c0"; }
+
+.fa-save:before {
+ content: "\f0c7"; }
+
+.fa-schlix:before {
+ content: "\f3ea"; }
+
+.fa-school:before {
+ content: "\f549"; }
+
+.fa-screwdriver:before {
+ content: "\f54a"; }
+
+.fa-scribd:before {
+ content: "\f28a"; }
+
+.fa-scroll:before {
+ content: "\f70e"; }
+
+.fa-sd-card:before {
+ content: "\f7c2"; }
+
+.fa-search:before {
+ content: "\f002"; }
+
+.fa-search-dollar:before {
+ content: "\f688"; }
+
+.fa-search-location:before {
+ content: "\f689"; }
+
+.fa-search-minus:before {
+ content: "\f010"; }
+
+.fa-search-plus:before {
+ content: "\f00e"; }
+
+.fa-searchengin:before {
+ content: "\f3eb"; }
+
+.fa-seedling:before {
+ content: "\f4d8"; }
+
+.fa-sellcast:before {
+ content: "\f2da"; }
+
+.fa-sellsy:before {
+ content: "\f213"; }
+
+.fa-server:before {
+ content: "\f233"; }
+
+.fa-servicestack:before {
+ content: "\f3ec"; }
+
+.fa-shapes:before {
+ content: "\f61f"; }
+
+.fa-share:before {
+ content: "\f064"; }
+
+.fa-share-alt:before {
+ content: "\f1e0"; }
+
+.fa-share-alt-square:before {
+ content: "\f1e1"; }
+
+.fa-share-square:before {
+ content: "\f14d"; }
+
+.fa-shekel-sign:before {
+ content: "\f20b"; }
+
+.fa-shield-alt:before {
+ content: "\f3ed"; }
+
+.fa-shield-virus:before {
+ content: "\e06c"; }
+
+.fa-ship:before {
+ content: "\f21a"; }
+
+.fa-shipping-fast:before {
+ content: "\f48b"; }
+
+.fa-shirtsinbulk:before {
+ content: "\f214"; }
+
+.fa-shoe-prints:before {
+ content: "\f54b"; }
+
+.fa-shopify:before {
+ content: "\e057"; }
+
+.fa-shopping-bag:before {
+ content: "\f290"; }
+
+.fa-shopping-basket:before {
+ content: "\f291"; }
+
+.fa-shopping-cart:before {
+ content: "\f07a"; }
+
+.fa-shopware:before {
+ content: "\f5b5"; }
+
+.fa-shower:before {
+ content: "\f2cc"; }
+
+.fa-shuttle-van:before {
+ content: "\f5b6"; }
+
+.fa-sign:before {
+ content: "\f4d9"; }
+
+.fa-sign-in-alt:before {
+ content: "\f2f6"; }
+
+.fa-sign-language:before {
+ content: "\f2a7"; }
+
+.fa-sign-out-alt:before {
+ content: "\f2f5"; }
+
+.fa-signal:before {
+ content: "\f012"; }
+
+.fa-signature:before {
+ content: "\f5b7"; }
+
+.fa-sim-card:before {
+ content: "\f7c4"; }
+
+.fa-simplybuilt:before {
+ content: "\f215"; }
+
+.fa-sink:before {
+ content: "\e06d"; }
+
+.fa-sistrix:before {
+ content: "\f3ee"; }
+
+.fa-sitemap:before {
+ content: "\f0e8"; }
+
+.fa-sith:before {
+ content: "\f512"; }
+
+.fa-skating:before {
+ content: "\f7c5"; }
+
+.fa-sketch:before {
+ content: "\f7c6"; }
+
+.fa-skiing:before {
+ content: "\f7c9"; }
+
+.fa-skiing-nordic:before {
+ content: "\f7ca"; }
+
+.fa-skull:before {
+ content: "\f54c"; }
+
+.fa-skull-crossbones:before {
+ content: "\f714"; }
+
+.fa-skyatlas:before {
+ content: "\f216"; }
+
+.fa-skype:before {
+ content: "\f17e"; }
+
+.fa-slack:before {
+ content: "\f198"; }
+
+.fa-slack-hash:before {
+ content: "\f3ef"; }
+
+.fa-slash:before {
+ content: "\f715"; }
+
+.fa-sleigh:before {
+ content: "\f7cc"; }
+
+.fa-sliders-h:before {
+ content: "\f1de"; }
+
+.fa-slideshare:before {
+ content: "\f1e7"; }
+
+.fa-smile:before {
+ content: "\f118"; }
+
+.fa-smile-beam:before {
+ content: "\f5b8"; }
+
+.fa-smile-wink:before {
+ content: "\f4da"; }
+
+.fa-smog:before {
+ content: "\f75f"; }
+
+.fa-smoking:before {
+ content: "\f48d"; }
+
+.fa-smoking-ban:before {
+ content: "\f54d"; }
+
+.fa-sms:before {
+ content: "\f7cd"; }
+
+.fa-snapchat:before {
+ content: "\f2ab"; }
+
+.fa-snapchat-ghost:before {
+ content: "\f2ac"; }
+
+.fa-snapchat-square:before {
+ content: "\f2ad"; }
+
+.fa-snowboarding:before {
+ content: "\f7ce"; }
+
+.fa-snowflake:before {
+ content: "\f2dc"; }
+
+.fa-snowman:before {
+ content: "\f7d0"; }
+
+.fa-snowplow:before {
+ content: "\f7d2"; }
+
+.fa-soap:before {
+ content: "\e06e"; }
+
+.fa-socks:before {
+ content: "\f696"; }
+
+.fa-solar-panel:before {
+ content: "\f5ba"; }
+
+.fa-sort:before {
+ content: "\f0dc"; }
+
+.fa-sort-alpha-down:before {
+ content: "\f15d"; }
+
+.fa-sort-alpha-down-alt:before {
+ content: "\f881"; }
+
+.fa-sort-alpha-up:before {
+ content: "\f15e"; }
+
+.fa-sort-alpha-up-alt:before {
+ content: "\f882"; }
+
+.fa-sort-amount-down:before {
+ content: "\f160"; }
+
+.fa-sort-amount-down-alt:before {
+ content: "\f884"; }
+
+.fa-sort-amount-up:before {
+ content: "\f161"; }
+
+.fa-sort-amount-up-alt:before {
+ content: "\f885"; }
+
+.fa-sort-down:before {
+ content: "\f0dd"; }
+
+.fa-sort-numeric-down:before {
+ content: "\f162"; }
+
+.fa-sort-numeric-down-alt:before {
+ content: "\f886"; }
+
+.fa-sort-numeric-up:before {
+ content: "\f163"; }
+
+.fa-sort-numeric-up-alt:before {
+ content: "\f887"; }
+
+.fa-sort-up:before {
+ content: "\f0de"; }
+
+.fa-soundcloud:before {
+ content: "\f1be"; }
+
+.fa-sourcetree:before {
+ content: "\f7d3"; }
+
+.fa-spa:before {
+ content: "\f5bb"; }
+
+.fa-space-shuttle:before {
+ content: "\f197"; }
+
+.fa-speakap:before {
+ content: "\f3f3"; }
+
+.fa-speaker-deck:before {
+ content: "\f83c"; }
+
+.fa-spell-check:before {
+ content: "\f891"; }
+
+.fa-spider:before {
+ content: "\f717"; }
+
+.fa-spinner:before {
+ content: "\f110"; }
+
+.fa-splotch:before {
+ content: "\f5bc"; }
+
+.fa-spotify:before {
+ content: "\f1bc"; }
+
+.fa-spray-can:before {
+ content: "\f5bd"; }
+
+.fa-square:before {
+ content: "\f0c8"; }
+
+.fa-square-full:before {
+ content: "\f45c"; }
+
+.fa-square-root-alt:before {
+ content: "\f698"; }
+
+.fa-squarespace:before {
+ content: "\f5be"; }
+
+.fa-stack-exchange:before {
+ content: "\f18d"; }
+
+.fa-stack-overflow:before {
+ content: "\f16c"; }
+
+.fa-stackpath:before {
+ content: "\f842"; }
+
+.fa-stamp:before {
+ content: "\f5bf"; }
+
+.fa-star:before {
+ content: "\f005"; }
+
+.fa-star-and-crescent:before {
+ content: "\f699"; }
+
+.fa-star-half:before {
+ content: "\f089"; }
+
+.fa-star-half-alt:before {
+ content: "\f5c0"; }
+
+.fa-star-of-david:before {
+ content: "\f69a"; }
+
+.fa-star-of-life:before {
+ content: "\f621"; }
+
+.fa-staylinked:before {
+ content: "\f3f5"; }
+
+.fa-steam:before {
+ content: "\f1b6"; }
+
+.fa-steam-square:before {
+ content: "\f1b7"; }
+
+.fa-steam-symbol:before {
+ content: "\f3f6"; }
+
+.fa-step-backward:before {
+ content: "\f048"; }
+
+.fa-step-forward:before {
+ content: "\f051"; }
+
+.fa-stethoscope:before {
+ content: "\f0f1"; }
+
+.fa-sticker-mule:before {
+ content: "\f3f7"; }
+
+.fa-sticky-note:before {
+ content: "\f249"; }
+
+.fa-stop:before {
+ content: "\f04d"; }
+
+.fa-stop-circle:before {
+ content: "\f28d"; }
+
+.fa-stopwatch:before {
+ content: "\f2f2"; }
+
+.fa-stopwatch-20:before {
+ content: "\e06f"; }
+
+.fa-store:before {
+ content: "\f54e"; }
+
+.fa-store-alt:before {
+ content: "\f54f"; }
+
+.fa-store-alt-slash:before {
+ content: "\e070"; }
+
+.fa-store-slash:before {
+ content: "\e071"; }
+
+.fa-strava:before {
+ content: "\f428"; }
+
+.fa-stream:before {
+ content: "\f550"; }
+
+.fa-street-view:before {
+ content: "\f21d"; }
+
+.fa-strikethrough:before {
+ content: "\f0cc"; }
+
+.fa-stripe:before {
+ content: "\f429"; }
+
+.fa-stripe-s:before {
+ content: "\f42a"; }
+
+.fa-stroopwafel:before {
+ content: "\f551"; }
+
+.fa-studiovinari:before {
+ content: "\f3f8"; }
+
+.fa-stumbleupon:before {
+ content: "\f1a4"; }
+
+.fa-stumbleupon-circle:before {
+ content: "\f1a3"; }
+
+.fa-subscript:before {
+ content: "\f12c"; }
+
+.fa-subway:before {
+ content: "\f239"; }
+
+.fa-suitcase:before {
+ content: "\f0f2"; }
+
+.fa-suitcase-rolling:before {
+ content: "\f5c1"; }
+
+.fa-sun:before {
+ content: "\f185"; }
+
+.fa-superpowers:before {
+ content: "\f2dd"; }
+
+.fa-superscript:before {
+ content: "\f12b"; }
+
+.fa-supple:before {
+ content: "\f3f9"; }
+
+.fa-surprise:before {
+ content: "\f5c2"; }
+
+.fa-suse:before {
+ content: "\f7d6"; }
+
+.fa-swatchbook:before {
+ content: "\f5c3"; }
+
+.fa-swift:before {
+ content: "\f8e1"; }
+
+.fa-swimmer:before {
+ content: "\f5c4"; }
+
+.fa-swimming-pool:before {
+ content: "\f5c5"; }
+
+.fa-symfony:before {
+ content: "\f83d"; }
+
+.fa-synagogue:before {
+ content: "\f69b"; }
+
+.fa-sync:before {
+ content: "\f021"; }
+
+.fa-sync-alt:before {
+ content: "\f2f1"; }
+
+.fa-syringe:before {
+ content: "\f48e"; }
+
+.fa-table:before {
+ content: "\f0ce"; }
+
+.fa-table-tennis:before {
+ content: "\f45d"; }
+
+.fa-tablet:before {
+ content: "\f10a"; }
+
+.fa-tablet-alt:before {
+ content: "\f3fa"; }
+
+.fa-tablets:before {
+ content: "\f490"; }
+
+.fa-tachometer-alt:before {
+ content: "\f3fd"; }
+
+.fa-tag:before {
+ content: "\f02b"; }
+
+.fa-tags:before {
+ content: "\f02c"; }
+
+.fa-tape:before {
+ content: "\f4db"; }
+
+.fa-tasks:before {
+ content: "\f0ae"; }
+
+.fa-taxi:before {
+ content: "\f1ba"; }
+
+.fa-teamspeak:before {
+ content: "\f4f9"; }
+
+.fa-teeth:before {
+ content: "\f62e"; }
+
+.fa-teeth-open:before {
+ content: "\f62f"; }
+
+.fa-telegram:before {
+ content: "\f2c6"; }
+
+.fa-telegram-plane:before {
+ content: "\f3fe"; }
+
+.fa-temperature-high:before {
+ content: "\f769"; }
+
+.fa-temperature-low:before {
+ content: "\f76b"; }
+
+.fa-tencent-weibo:before {
+ content: "\f1d5"; }
+
+.fa-tenge:before {
+ content: "\f7d7"; }
+
+.fa-terminal:before {
+ content: "\f120"; }
+
+.fa-text-height:before {
+ content: "\f034"; }
+
+.fa-text-width:before {
+ content: "\f035"; }
+
+.fa-th:before {
+ content: "\f00a"; }
+
+.fa-th-large:before {
+ content: "\f009"; }
+
+.fa-th-list:before {
+ content: "\f00b"; }
+
+.fa-the-red-yeti:before {
+ content: "\f69d"; }
+
+.fa-theater-masks:before {
+ content: "\f630"; }
+
+.fa-themeco:before {
+ content: "\f5c6"; }
+
+.fa-themeisle:before {
+ content: "\f2b2"; }
+
+.fa-thermometer:before {
+ content: "\f491"; }
+
+.fa-thermometer-empty:before {
+ content: "\f2cb"; }
+
+.fa-thermometer-full:before {
+ content: "\f2c7"; }
+
+.fa-thermometer-half:before {
+ content: "\f2c9"; }
+
+.fa-thermometer-quarter:before {
+ content: "\f2ca"; }
+
+.fa-thermometer-three-quarters:before {
+ content: "\f2c8"; }
+
+.fa-think-peaks:before {
+ content: "\f731"; }
+
+.fa-thumbs-down:before {
+ content: "\f165"; }
+
+.fa-thumbs-up:before {
+ content: "\f164"; }
+
+.fa-thumbtack:before {
+ content: "\f08d"; }
+
+.fa-ticket-alt:before {
+ content: "\f3ff"; }
+
+.fa-tiktok:before {
+ content: "\e07b"; }
+
+.fa-times:before {
+ content: "\f00d"; }
+
+.fa-times-circle:before {
+ content: "\f057"; }
+
+.fa-tint:before {
+ content: "\f043"; }
+
+.fa-tint-slash:before {
+ content: "\f5c7"; }
+
+.fa-tired:before {
+ content: "\f5c8"; }
+
+.fa-toggle-off:before {
+ content: "\f204"; }
+
+.fa-toggle-on:before {
+ content: "\f205"; }
+
+.fa-toilet:before {
+ content: "\f7d8"; }
+
+.fa-toilet-paper:before {
+ content: "\f71e"; }
+
+.fa-toilet-paper-slash:before {
+ content: "\e072"; }
+
+.fa-toolbox:before {
+ content: "\f552"; }
+
+.fa-tools:before {
+ content: "\f7d9"; }
+
+.fa-tooth:before {
+ content: "\f5c9"; }
+
+.fa-torah:before {
+ content: "\f6a0"; }
+
+.fa-torii-gate:before {
+ content: "\f6a1"; }
+
+.fa-tractor:before {
+ content: "\f722"; }
+
+.fa-trade-federation:before {
+ content: "\f513"; }
+
+.fa-trademark:before {
+ content: "\f25c"; }
+
+.fa-traffic-light:before {
+ content: "\f637"; }
+
+.fa-trailer:before {
+ content: "\e041"; }
+
+.fa-train:before {
+ content: "\f238"; }
+
+.fa-tram:before {
+ content: "\f7da"; }
+
+.fa-transgender:before {
+ content: "\f224"; }
+
+.fa-transgender-alt:before {
+ content: "\f225"; }
+
+.fa-trash:before {
+ content: "\f1f8"; }
+
+.fa-trash-alt:before {
+ content: "\f2ed"; }
+
+.fa-trash-restore:before {
+ content: "\f829"; }
+
+.fa-trash-restore-alt:before {
+ content: "\f82a"; }
+
+.fa-tree:before {
+ content: "\f1bb"; }
+
+.fa-trello:before {
+ content: "\f181"; }
+
+.fa-tripadvisor:before {
+ content: "\f262"; }
+
+.fa-trophy:before {
+ content: "\f091"; }
+
+.fa-truck:before {
+ content: "\f0d1"; }
+
+.fa-truck-loading:before {
+ content: "\f4de"; }
+
+.fa-truck-monster:before {
+ content: "\f63b"; }
+
+.fa-truck-moving:before {
+ content: "\f4df"; }
+
+.fa-truck-pickup:before {
+ content: "\f63c"; }
+
+.fa-tshirt:before {
+ content: "\f553"; }
+
+.fa-tty:before {
+ content: "\f1e4"; }
+
+.fa-tumblr:before {
+ content: "\f173"; }
+
+.fa-tumblr-square:before {
+ content: "\f174"; }
+
+.fa-tv:before {
+ content: "\f26c"; }
+
+.fa-twitch:before {
+ content: "\f1e8"; }
+
+.fa-twitter:before {
+ content: "\f099"; }
+
+.fa-twitter-square:before {
+ content: "\f081"; }
+
+.fa-typo3:before {
+ content: "\f42b"; }
+
+.fa-uber:before {
+ content: "\f402"; }
+
+.fa-ubuntu:before {
+ content: "\f7df"; }
+
+.fa-uikit:before {
+ content: "\f403"; }
+
+.fa-umbraco:before {
+ content: "\f8e8"; }
+
+.fa-umbrella:before {
+ content: "\f0e9"; }
+
+.fa-umbrella-beach:before {
+ content: "\f5ca"; }
+
+.fa-uncharted:before {
+ content: "\e084"; }
+
+.fa-underline:before {
+ content: "\f0cd"; }
+
+.fa-undo:before {
+ content: "\f0e2"; }
+
+.fa-undo-alt:before {
+ content: "\f2ea"; }
+
+.fa-uniregistry:before {
+ content: "\f404"; }
+
+.fa-unity:before {
+ content: "\e049"; }
+
+.fa-universal-access:before {
+ content: "\f29a"; }
+
+.fa-university:before {
+ content: "\f19c"; }
+
+.fa-unlink:before {
+ content: "\f127"; }
+
+.fa-unlock:before {
+ content: "\f09c"; }
+
+.fa-unlock-alt:before {
+ content: "\f13e"; }
+
+.fa-unsplash:before {
+ content: "\e07c"; }
+
+.fa-untappd:before {
+ content: "\f405"; }
+
+.fa-upload:before {
+ content: "\f093"; }
+
+.fa-ups:before {
+ content: "\f7e0"; }
+
+.fa-usb:before {
+ content: "\f287"; }
+
+.fa-user:before {
+ content: "\f007"; }
+
+.fa-user-alt:before {
+ content: "\f406"; }
+
+.fa-user-alt-slash:before {
+ content: "\f4fa"; }
+
+.fa-user-astronaut:before {
+ content: "\f4fb"; }
+
+.fa-user-check:before {
+ content: "\f4fc"; }
+
+.fa-user-circle:before {
+ content: "\f2bd"; }
+
+.fa-user-clock:before {
+ content: "\f4fd"; }
+
+.fa-user-cog:before {
+ content: "\f4fe"; }
+
+.fa-user-edit:before {
+ content: "\f4ff"; }
+
+.fa-user-friends:before {
+ content: "\f500"; }
+
+.fa-user-graduate:before {
+ content: "\f501"; }
+
+.fa-user-injured:before {
+ content: "\f728"; }
+
+.fa-user-lock:before {
+ content: "\f502"; }
+
+.fa-user-md:before {
+ content: "\f0f0"; }
+
+.fa-user-minus:before {
+ content: "\f503"; }
+
+.fa-user-ninja:before {
+ content: "\f504"; }
+
+.fa-user-nurse:before {
+ content: "\f82f"; }
+
+.fa-user-plus:before {
+ content: "\f234"; }
+
+.fa-user-secret:before {
+ content: "\f21b"; }
+
+.fa-user-shield:before {
+ content: "\f505"; }
+
+.fa-user-slash:before {
+ content: "\f506"; }
+
+.fa-user-tag:before {
+ content: "\f507"; }
+
+.fa-user-tie:before {
+ content: "\f508"; }
+
+.fa-user-times:before {
+ content: "\f235"; }
+
+.fa-users:before {
+ content: "\f0c0"; }
+
+.fa-users-cog:before {
+ content: "\f509"; }
+
+.fa-users-slash:before {
+ content: "\e073"; }
+
+.fa-usps:before {
+ content: "\f7e1"; }
+
+.fa-ussunnah:before {
+ content: "\f407"; }
+
+.fa-utensil-spoon:before {
+ content: "\f2e5"; }
+
+.fa-utensils:before {
+ content: "\f2e7"; }
+
+.fa-vaadin:before {
+ content: "\f408"; }
+
+.fa-vector-square:before {
+ content: "\f5cb"; }
+
+.fa-venus:before {
+ content: "\f221"; }
+
+.fa-venus-double:before {
+ content: "\f226"; }
+
+.fa-venus-mars:before {
+ content: "\f228"; }
+
+.fa-vest:before {
+ content: "\e085"; }
+
+.fa-vest-patches:before {
+ content: "\e086"; }
+
+.fa-viacoin:before {
+ content: "\f237"; }
+
+.fa-viadeo:before {
+ content: "\f2a9"; }
+
+.fa-viadeo-square:before {
+ content: "\f2aa"; }
+
+.fa-vial:before {
+ content: "\f492"; }
+
+.fa-vials:before {
+ content: "\f493"; }
+
+.fa-viber:before {
+ content: "\f409"; }
+
+.fa-video:before {
+ content: "\f03d"; }
+
+.fa-video-slash:before {
+ content: "\f4e2"; }
+
+.fa-vihara:before {
+ content: "\f6a7"; }
+
+.fa-vimeo:before {
+ content: "\f40a"; }
+
+.fa-vimeo-square:before {
+ content: "\f194"; }
+
+.fa-vimeo-v:before {
+ content: "\f27d"; }
+
+.fa-vine:before {
+ content: "\f1ca"; }
+
+.fa-virus:before {
+ content: "\e074"; }
+
+.fa-virus-slash:before {
+ content: "\e075"; }
+
+.fa-viruses:before {
+ content: "\e076"; }
+
+.fa-vk:before {
+ content: "\f189"; }
+
+.fa-vnv:before {
+ content: "\f40b"; }
+
+.fa-voicemail:before {
+ content: "\f897"; }
+
+.fa-volleyball-ball:before {
+ content: "\f45f"; }
+
+.fa-volume-down:before {
+ content: "\f027"; }
+
+.fa-volume-mute:before {
+ content: "\f6a9"; }
+
+.fa-volume-off:before {
+ content: "\f026"; }
+
+.fa-volume-up:before {
+ content: "\f028"; }
+
+.fa-vote-yea:before {
+ content: "\f772"; }
+
+.fa-vr-cardboard:before {
+ content: "\f729"; }
+
+.fa-vuejs:before {
+ content: "\f41f"; }
+
+.fa-walking:before {
+ content: "\f554"; }
+
+.fa-wallet:before {
+ content: "\f555"; }
+
+.fa-warehouse:before {
+ content: "\f494"; }
+
+.fa-watchman-monitoring:before {
+ content: "\e087"; }
+
+.fa-water:before {
+ content: "\f773"; }
+
+.fa-wave-square:before {
+ content: "\f83e"; }
+
+.fa-waze:before {
+ content: "\f83f"; }
+
+.fa-weebly:before {
+ content: "\f5cc"; }
+
+.fa-weibo:before {
+ content: "\f18a"; }
+
+.fa-weight:before {
+ content: "\f496"; }
+
+.fa-weight-hanging:before {
+ content: "\f5cd"; }
+
+.fa-weixin:before {
+ content: "\f1d7"; }
+
+.fa-whatsapp:before {
+ content: "\f232"; }
+
+.fa-whatsapp-square:before {
+ content: "\f40c"; }
+
+.fa-wheelchair:before {
+ content: "\f193"; }
+
+.fa-whmcs:before {
+ content: "\f40d"; }
+
+.fa-wifi:before {
+ content: "\f1eb"; }
+
+.fa-wikipedia-w:before {
+ content: "\f266"; }
+
+.fa-wind:before {
+ content: "\f72e"; }
+
+.fa-window-close:before {
+ content: "\f410"; }
+
+.fa-window-maximize:before {
+ content: "\f2d0"; }
+
+.fa-window-minimize:before {
+ content: "\f2d1"; }
+
+.fa-window-restore:before {
+ content: "\f2d2"; }
+
+.fa-windows:before {
+ content: "\f17a"; }
+
+.fa-wine-bottle:before {
+ content: "\f72f"; }
+
+.fa-wine-glass:before {
+ content: "\f4e3"; }
+
+.fa-wine-glass-alt:before {
+ content: "\f5ce"; }
+
+.fa-wix:before {
+ content: "\f5cf"; }
+
+.fa-wizards-of-the-coast:before {
+ content: "\f730"; }
+
+.fa-wodu:before {
+ content: "\e088"; }
+
+.fa-wolf-pack-battalion:before {
+ content: "\f514"; }
+
+.fa-won-sign:before {
+ content: "\f159"; }
+
+.fa-wordpress:before {
+ content: "\f19a"; }
+
+.fa-wordpress-simple:before {
+ content: "\f411"; }
+
+.fa-wpbeginner:before {
+ content: "\f297"; }
+
+.fa-wpexplorer:before {
+ content: "\f2de"; }
+
+.fa-wpforms:before {
+ content: "\f298"; }
+
+.fa-wpressr:before {
+ content: "\f3e4"; }
+
+.fa-wrench:before {
+ content: "\f0ad"; }
+
+.fa-x-ray:before {
+ content: "\f497"; }
+
+.fa-xbox:before {
+ content: "\f412"; }
+
+.fa-xing:before {
+ content: "\f168"; }
+
+.fa-xing-square:before {
+ content: "\f169"; }
+
+.fa-y-combinator:before {
+ content: "\f23b"; }
+
+.fa-yahoo:before {
+ content: "\f19e"; }
+
+.fa-yammer:before {
+ content: "\f840"; }
+
+.fa-yandex:before {
+ content: "\f413"; }
+
+.fa-yandex-international:before {
+ content: "\f414"; }
+
+.fa-yarn:before {
+ content: "\f7e3"; }
+
+.fa-yelp:before {
+ content: "\f1e9"; }
+
+.fa-yen-sign:before {
+ content: "\f157"; }
+
+.fa-yin-yang:before {
+ content: "\f6ad"; }
+
+.fa-yoast:before {
+ content: "\f2b1"; }
+
+.fa-youtube:before {
+ content: "\f167"; }
+
+.fa-youtube-square:before {
+ content: "\f431"; }
+
+.fa-zhihu:before {
+ content: "\f63f"; }
+
+.sr-only {
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px; }
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+ clip: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ position: static;
+ width: auto; }
+@font-face {
+ font-family: 'Font Awesome 5 Brands';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url("webfonts/fa-brands-400.eot");
+ src: url("webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("webfonts/fa-brands-400.woff2") format("woff2"), url("webfonts/fa-brands-400.woff") format("woff"), url("webfonts/fa-brands-400.ttf") format("truetype"), url("webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
+
+.fab {
+ font-family: 'Font Awesome 5 Brands';
+ font-weight: 400; }
+@font-face {
+ font-family: 'Font Awesome 5 Free';
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url("webfonts/fa-regular-400.eot");
+ src: url("webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("webfonts/fa-regular-400.woff2") format("woff2"), url("webfonts/fa-regular-400.woff") format("woff"), url("webfonts/fa-regular-400.ttf") format("truetype"), url("webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
+
+.far {
+ font-family: 'Font Awesome 5 Free';
+ font-weight: 400; }
+@font-face {
+ font-family: 'Font Awesome 5 Free';
+ font-style: normal;
+ font-weight: 900;
+ font-display: block;
+ src: url("webfonts/fa-solid-900.eot");
+ src: url("webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("webfonts/fa-solid-900.woff2") format("woff2"), url("webfonts/fa-solid-900.woff") format("woff"), url("webfonts/fa-solid-900.ttf") format("truetype"), url("webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
+
+.fa,
+.fas {
+ font-family: 'Font Awesome 5 Free';
+ font-weight: 900; }
diff --git a/gui/img/__init__.py b/gui/img/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/img/logo.png b/gui/img/logo.png
new file mode 100644
index 0000000..cb7a282
Binary files /dev/null and b/gui/img/logo.png differ
diff --git a/gui/js/__init__.py b/gui/js/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/js/app.js b/gui/js/app.js
new file mode 100644
index 0000000..e8f5912
--- /dev/null
+++ b/gui/js/app.js
@@ -0,0 +1,158 @@
+
+// Create Flexx elements inline
+if (!customElements.get("x-flx")) {
+ class Flexx extends HTMLDivElement {
+ constructor() {
+ super();
+ }
+ }
+
+ customElements.define("x-flx", Flexx, { extends: 'div' })
+}
+
+let app = null;
+let react = null;
+
+const reqHeaders = {
+ headers: {
+ 'User-Agent': 'ProjectBorealis-PBSync',
+ },
+}
+
+let reqGHAPIHeaders = {
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json',
+ 'Content-Type': 'application/json;charset=UTF-8',
+ ...reqHeaders.headers
+ }
+}
+
+// React function helpers
+function changePage(page) {
+ react("change_page", page)
+}
+
+function markCommit(sha, status) {
+ console.log("sha")
+ let commitNode = document.getElementById(sha)
+ if (status == "success") {
+ commitNode.parentElement.classList.remove("table-danger")
+ commitNode.children[0].classList.add("fa-check-circle")
+ commitNode.children[0].classList.remove("fa-times-circle")
+ } else if (status == "failure") {
+ commitNode.parentElement.classList.add("table-danger")
+ commitNode.children[0].classList.add("fa-times-circle")
+ commitNode.children[0].classList.remove("fa-check-circle")
+ }
+ react("mark_commit", sha, status)
+}
+
+// Element handlers
+window.elementHandlers = {
+};
+
+window.post_commit_update = function(el) {
+ let table = el.children[0];
+ let tbody = table.children[1];
+ for (const row of tbody.children) {
+ if (row.nodeName != "TR") {
+ continue;
+ }
+ let commitNode = row.children[0];
+ let status = row.dataset.status;
+ let commit = commitNode.id;
+ // try getting our metadata status, if we have it
+ if (status) {
+ if (status == "success") {
+ commitNode.children[0].classList.add("fa-check-circle");
+ } else if (status == "failure") {
+ row.classList.add("table-danger");
+ commitNode.children[0].classList.add("fa-times-circle");
+ }
+ } else {
+ // fetch build status from github
+ fetch("https://api.github.com/repos/" + window.GH_REPO + "/commits/" + commit + "/check-suites?app_id=15368", reqGHAPIHeaders)
+ .then((response) => response.json())
+ .then((data) => {
+ let status = "success";
+ let checks = 0;
+ for (const check of data.check_suites) {
+ if (check.status == "completed") {
+ checks++;
+ if (check.conclusion != "success") {
+ status = ""
+ } else if (data.conclusion == "failure") {
+ status = "failure"
+ break;
+ }
+ }
+ }
+ if (checks > 1) {
+ if (status == "success") {
+ commitNode.children[0].classList.add("fa-check-circle");
+ } else if (status == "failure") {
+ row.classList.add("table-danger");
+ commitNode.children[0].classList.add("fa-times-circle");
+ }
+ }
+ });
+ }
+ let dropdown = row.children[row.children.length - 1].children[0].children[1].children[0]; // td -> row -> col-1 -> dropdown
+ let dropdownBtn = dropdown.children[0];
+ dropdownBtn.onclick = () => {
+ bootstrap.Dropdown.getOrCreateInstance(dropdownBtn).toggle();
+ row.classList.toggle("active");
+ }
+ let menuItems = dropdown.children[1].children;
+ for (const menuItem of menuItems) {
+ menuItem.children[0].onclick = () => {
+ if (menuItem.dataset.func == "mark_commit") {
+ markCommit(commit, menuItem.dataset.status)
+ }
+ }
+ }
+ }
+}
+
+// Hook up JavaScript communication
+// and callback to notify Python
+function callback() {
+ console.log("App loaded")
+ app = document.getElementById("app")
+ react = app.onreact
+ reqGHAPIHeaders.headers.Authorization = window.GH_AUTH
+
+ react("app_update")
+
+ let observer = new MutationObserver(() => {
+ react("app_update")
+ })
+
+ observer.observe(app, {
+ subtree: true,
+ attributes: true,
+ childList: true,
+ characterData: true
+ })
+}
+
+let observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (!mutation.addedNodes) return
+
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
+ // do things to your newly added nodes here
+ let node = mutation.addedNodes[i]
+ if (node.id === "app") {
+ observer.disconnect()
+ callback()
+ return
+ }
+ }
+ })
+})
+
+observer.observe(document.body, {
+ childList: true,
+ subtree: true
+})
\ No newline at end of file
diff --git a/gui/templates/__init__.py b/gui/templates/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/templates/build.html b/gui/templates/build.html
new file mode 100644
index 0000000..e69de29
diff --git a/gui/templates/filter.html b/gui/templates/filter.html
new file mode 100644
index 0000000..e69de29
diff --git a/gui/templates/logo.html b/gui/templates/logo.html
new file mode 100644
index 0000000..3d41eee
--- /dev/null
+++ b/gui/templates/logo.html
@@ -0,0 +1 @@
+
PBSync
diff --git a/gui/templates/navbar.html b/gui/templates/navbar.html
new file mode 100644
index 0000000..301fca9
--- /dev/null
+++ b/gui/templates/navbar.html
@@ -0,0 +1,90 @@
+
diff --git a/gui/templates/settings.html b/gui/templates/settings.html
new file mode 100644
index 0000000..a5afd91
--- /dev/null
+++ b/gui/templates/settings.html
@@ -0,0 +1,8 @@
+{% include 'navbar.html' %}
+
diff --git a/gui/templates/submit.html b/gui/templates/submit.html
new file mode 100644
index 0000000..a5afd91
--- /dev/null
+++ b/gui/templates/submit.html
@@ -0,0 +1,8 @@
+{% include 'navbar.html' %}
+
diff --git a/gui/templates/sync.html b/gui/templates/sync.html
new file mode 100644
index 0000000..cef6ee2
--- /dev/null
+++ b/gui/templates/sync.html
@@ -0,0 +1,8 @@
+{% include 'navbar.html' %}
+
diff --git a/gui/webfonts/Roboto-Regular.ttf b/gui/webfonts/Roboto-Regular.ttf
new file mode 100644
index 0000000..2b6392f
Binary files /dev/null and b/gui/webfonts/Roboto-Regular.ttf differ
diff --git a/gui/webfonts/__init__.py b/gui/webfonts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gui/webfonts/fa-brands-400.eot b/gui/webfonts/fa-brands-400.eot
new file mode 100644
index 0000000..958684e
Binary files /dev/null and b/gui/webfonts/fa-brands-400.eot differ
diff --git a/gui/webfonts/fa-brands-400.svg b/gui/webfonts/fa-brands-400.svg
new file mode 100644
index 0000000..2b7cf17
--- /dev/null
+++ b/gui/webfonts/fa-brands-400.svg
@@ -0,0 +1,3717 @@
+
+
+
diff --git a/gui/webfonts/fa-brands-400.ttf b/gui/webfonts/fa-brands-400.ttf
new file mode 100644
index 0000000..f071825
Binary files /dev/null and b/gui/webfonts/fa-brands-400.ttf differ
diff --git a/gui/webfonts/fa-brands-400.woff b/gui/webfonts/fa-brands-400.woff
new file mode 100644
index 0000000..277ab65
Binary files /dev/null and b/gui/webfonts/fa-brands-400.woff differ
diff --git a/gui/webfonts/fa-brands-400.woff2 b/gui/webfonts/fa-brands-400.woff2
new file mode 100644
index 0000000..47805d4
Binary files /dev/null and b/gui/webfonts/fa-brands-400.woff2 differ
diff --git a/gui/webfonts/fa-regular-400.eot b/gui/webfonts/fa-regular-400.eot
new file mode 100644
index 0000000..bef9f72
Binary files /dev/null and b/gui/webfonts/fa-regular-400.eot differ
diff --git a/gui/webfonts/fa-regular-400.svg b/gui/webfonts/fa-regular-400.svg
new file mode 100644
index 0000000..bccc256
--- /dev/null
+++ b/gui/webfonts/fa-regular-400.svg
@@ -0,0 +1,801 @@
+
+
+
diff --git a/gui/webfonts/fa-regular-400.ttf b/gui/webfonts/fa-regular-400.ttf
new file mode 100644
index 0000000..659527a
Binary files /dev/null and b/gui/webfonts/fa-regular-400.ttf differ
diff --git a/gui/webfonts/fa-regular-400.woff b/gui/webfonts/fa-regular-400.woff
new file mode 100644
index 0000000..31f44b2
Binary files /dev/null and b/gui/webfonts/fa-regular-400.woff differ
diff --git a/gui/webfonts/fa-regular-400.woff2 b/gui/webfonts/fa-regular-400.woff2
new file mode 100644
index 0000000..0332a9b
Binary files /dev/null and b/gui/webfonts/fa-regular-400.woff2 differ
diff --git a/gui/webfonts/fa-solid-900.eot b/gui/webfonts/fa-solid-900.eot
new file mode 100644
index 0000000..5da4fa0
Binary files /dev/null and b/gui/webfonts/fa-solid-900.eot differ
diff --git a/gui/webfonts/fa-solid-900.svg b/gui/webfonts/fa-solid-900.svg
new file mode 100644
index 0000000..313b311
--- /dev/null
+++ b/gui/webfonts/fa-solid-900.svg
@@ -0,0 +1,5028 @@
+
+
+
diff --git a/gui/webfonts/fa-solid-900.ttf b/gui/webfonts/fa-solid-900.ttf
new file mode 100644
index 0000000..e074608
Binary files /dev/null and b/gui/webfonts/fa-solid-900.ttf differ
diff --git a/gui/webfonts/fa-solid-900.woff b/gui/webfonts/fa-solid-900.woff
new file mode 100644
index 0000000..ef6b447
Binary files /dev/null and b/gui/webfonts/fa-solid-900.woff differ
diff --git a/gui/webfonts/fa-solid-900.woff2 b/gui/webfonts/fa-solid-900.woff2
new file mode 100644
index 0000000..120b300
Binary files /dev/null and b/gui/webfonts/fa-solid-900.woff2 differ
diff --git a/pbgui/__init__.py b/pbgui/__init__.py
new file mode 100644
index 0000000..f3214e0
--- /dev/null
+++ b/pbgui/__init__.py
@@ -0,0 +1,55 @@
+from logging import getLogger
+from pathlib import Path
+
+from flexx import flx
+
+import importlib.resources as pkg_resources
+import gui
+import gui.webfonts
+import gui.img
+
+from jinja2 import Environment, PackageLoader, select_autoescape
+
+env = Environment(
+ loader=PackageLoader('gui', 'templates'),
+ autoescape=select_autoescape(['html', 'xml'])
+)
+
+log = getLogger(__name__)
+
+asset_pkgs = [("webfonts/", gui.webfonts), ("img/", gui.img)]
+
+m = None
+default_page = "sync"
+sync_fn = None
+
+def load_flexx_static(data):
+ for asset_dir, _ in asset_pkgs:
+ data = data.replace(asset_dir, f"/flexx/data/shared/{asset_dir}/")
+ return data
+
+
+def load_template(filename, kwargs):
+ template = env.get_template(filename)
+ print(kwargs)
+ return load_flexx_static(template.render(**kwargs))
+
+
+def load_static(pkg, filename):
+ data = pkg_resources.read_text(pkg, filename)
+ return load_flexx_static(data)
+
+
+def set_default_page(page):
+ global default_page
+ default_page = page
+
+
+for asset_dir, asset_pkg in asset_pkgs:
+ for asset_name in pkg_resources.contents(asset_pkg):
+ # TODO hack: no folders
+ if "." not in asset_name:
+ continue
+
+ print("Loaded shared asset " +
+ flx.assets.add_shared_data(f"{asset_dir}{asset_name}", pkg_resources.read_binary(asset_pkg, asset_name)))
diff --git a/pbgui/core.py b/pbgui/core.py
new file mode 100644
index 0000000..ec62a9e
--- /dev/null
+++ b/pbgui/core.py
@@ -0,0 +1,90 @@
+import subprocess, os, platform
+import json
+
+import humanhash
+
+from flexx import flx
+from flexx.ui import FileBrowserWidget
+from pathlib import Path
+
+from pbpy import pbgit
+from pbpy import pbtools
+
+import pbgui
+from pbgui.gateway import Gateway
+
+metafile = ".github/pbsync-meta.json"
+
+class Core(flx.PyWidget):
+ FilePath = ""
+
+ def init(self):
+ user, token = pbgit.get_credentials()
+ repo = pbgit.get_remote_url().replace(".git", "").replace("https://github.com/", "")
+ self.g = Gateway(gh_user=user, gh_token=token, gh_repo=repo)
+ self.fs = FileBrowserWidget()
+ self.g.set_jfs(self.fs._jswidget)
+ self.g.init_page(pbgui.default_page)
+
+ @flx.action
+ def open_file(self, filename):
+ filepath = str(Path(filename).resolve())
+ if platform.system() == 'Darwin': # macOS
+ subprocess.call(('open', filepath))
+ elif platform.system() == 'Windows': # Windows
+ os.startfile(filepath)
+ else: # linux variants
+ subprocess.call(('xdg-open', filepath))
+
+ @flx.reaction('fs.selected')
+ def fileselect(self, *events):
+ sfile = events[-1] # shows the path
+ self.FilePath = sfile.filename
+
+ @flx.action
+ def get_commits(self):
+ data = None
+ with open(metafile) as f:
+ data = json.load(f)
+ commits = []
+ # go through all commits to update the log
+ lines = pbgit.get_commits().splitlines()
+ local_commit = pbtools.get_one_line_output([pbgit.get_git_executable(), "rev-parse", "HEAD"])
+ commit = None
+ need_message = False
+ for line in lines:
+ line = line.strip()
+ # start new commit entry
+ if line.startswith("commit"):
+ if commit:
+ commits.append(commit)
+ sha = line.split(" ")[1]
+ commit = {"sha": sha, "human": humanhash.humanize(sha, words=2)}
+ if sha == local_commit:
+ commit["local"] = True
+ obj = data.get(sha)
+ if obj:
+ commit["status"] = data["status"]
+ elif line.startswith("Author"):
+ commit["author"] = line.split(" ", 1)[1].rsplit("<", 1)[0][:-1]
+ elif line.startswith("Date"):
+ time = line.split(" ", 3)[3]
+ commit["time"] = time.rsplit(" ", 1)[0]
+ need_message = True
+ elif line and need_message:
+ commit["message"] = line
+ need_message = False
+
+ self.g.update_commits(commits)
+
+ @flx.action
+ def mark_commit(self, sha, status):
+ data = None
+ with open(metafile, "w") as f:
+ data = json.load(f)
+ obj = data.get(sha)
+ if obj:
+ obj["status"] = status
+ else:
+ data[sha] = {"status": status}
+ json.dump(data, f)
diff --git a/pbgui/gateway.py b/pbgui/gateway.py
new file mode 100644
index 0000000..1d7d9c3
--- /dev/null
+++ b/pbgui/gateway.py
@@ -0,0 +1,150 @@
+from flexx import flx
+from pscript import RawJS
+
+import importlib.resources as pkg_resources
+import gui
+import gui.js
+import gui.css
+import gui.templates
+
+from pbgui import load_static, load_template, widgets
+
+pages = {}
+
+
+def load_templated_page(file, name, kwargs):
+ print(f"Loaded template {file}.html ({name})")
+ pages[name] = load_template(f"{file}.html", kwargs)
+
+# Base HTML page name -> list of virtual page names
+virtual_pages = {
+}
+
+# Jinja2 static properties to define per page
+page_props = {
+ "d": {}
+}
+
+for resource in pkg_resources.contents(gui.templates):
+ if "." not in resource:
+ continue
+ with pkg_resources.path(gui.templates, resource) as page:
+ vpages = [page.stem]
+ if page.stem in virtual_pages:
+ vpages.extend(virtual_pages[page.stem])
+ for vpage in vpages:
+ if page.is_file() and page.suffix == ".html":
+ props = page_props["d"]
+ if vpage in page_props:
+ props = {**props, **page_props[vpage]}
+ props["PAGE"] = vpage
+ load_templated_page(page.stem, vpage, props)
+
+flx.assets.associate_asset("pbgui.gateway", "js/app.js", lambda: load_static(gui.js, "app.js"))
+
+remote_assets = ["https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"]
+
+for url in remote_assets:
+ flx.assets.associate_asset("pbgui.gateway", url)
+
+
+class Gateway(flx.Label):
+ CSS = load_static(gui.css, "font-awesome.css") + load_static(gui.css, "app.css")
+
+ actions = {}
+
+ elements = {}
+
+ page_elements = []
+
+ widgets = {}
+
+ jfs = None
+
+ gh_user = flx.StringProp()
+ gh_token = flx.StringProp()
+ gh_repo = flx.StringProp()
+
+ def init(self):
+ self.actions = {
+ "change_page": self.change_page,
+ "app_update": self.app_update,
+ "mark_commit": self.mark_commit,
+ }
+ self.elements = {
+ "Button": flx.Button,
+ "FileWidget": None,
+ "CommitLogTable": widgets.CommitLogTableWidget,
+ "Settings": widgets.SettingsWidget,
+ }
+
+ def _create_dom(self):
+ global window
+ window.GH_USER = self.gh_user
+ window.GH_AUTH = "token " + self.gh_token
+ window.GH_REPO = self.gh_repo
+ return flx.create_element("div", {"id": "app", "onreact": self.react})
+
+ def _render_dom(self):
+ return None
+
+ def react(self, action, *data):
+ if action in self.actions:
+ return self.actions[action](*data)
+ else:
+ print(f"{action} not found!")
+
+ def change_page(self, page):
+ for element in self.page_elements:
+ element.outernode.remove()
+ element.dispose()
+ self.page_elements.clear()
+ self.widgets.clear()
+ self.set_html(pages[page])
+
+ def get_widget(self, element_id):
+ return self.widgets[element_id]
+
+ def app_update(self):
+ global window
+ flexx_elements = window.document.querySelectorAll("x-flx")
+ construct = RawJS("Reflect.construct")
+ self.__enter__()
+ for i in flexx_elements:
+ element = flexx_elements[i]
+ el_name = element.getAttribute("el")
+ if el_name in self.elements:
+ kwargs = {}
+ for data in element.dataset:
+ kwargs[data] = element.dataset[data]
+ flx_node = None
+ if el_name == "FileWidget":
+ flx_node = self.jfs
+ else:
+ constructor = self.elements[el_name]
+ flx_node = construct(constructor, [{"flx_args": [], "flx_kwargs": kwargs}])
+ self.page_elements.append(flx_node)
+ element_id = element.getAttribute("id")
+ if element_id is not None:
+ self.widgets[element_id] = flx_node
+ flx_node.outernode.id = element_id
+ element.after(flx_node.outernode)
+ if window.elementHandlers.hasOwnProperty(el_name):
+ window.elementHandlers[el_name](flx_node.outernode)
+ element.remove()
+ self.__exit__()
+
+ def mark_commit(self, sha, status):
+ self.root.mark_commit(sha, status)
+
+ @flx.action
+ def set_jfs(self, filebrowser):
+ self.jfs = filebrowser
+
+ @flx.action
+ def update_commits(self, commits):
+ self.get_widget("commit-log").update_commits(commits)
+
+ @flx.action
+ def init_page(self, page):
+ self.set_html(pages[page])
diff --git a/pbgui/main.py b/pbgui/main.py
new file mode 100644
index 0000000..c6e6d11
--- /dev/null
+++ b/pbgui/main.py
@@ -0,0 +1,26 @@
+from pbgui.core import Core
+from flexx import flx
+import pbgui
+import logging
+import flexx
+import asyncio
+
+flexx.config.log_level = logging.INFO
+
+
+def run_flexx():
+ a = flx.App(Core, title='PBSync')
+ pbgui.m = a.launch(runtime='chrome-browser')
+
+
+def startup():
+ pass
+
+
+def run(sync):
+ pbgui.sync_fn = sync
+ run_flexx()
+
+ asyncio.get_event_loop().call_soon(startup)
+
+ flx.run()
diff --git a/pbgui/widgets/__init__.py b/pbgui/widgets/__init__.py
new file mode 100644
index 0000000..b24495b
--- /dev/null
+++ b/pbgui/widgets/__init__.py
@@ -0,0 +1,2 @@
+from .commitlog import CommitLogTableWidget
+from .settings import SettingsWidget
\ No newline at end of file
diff --git a/pbgui/widgets/commitlog.py b/pbgui/widgets/commitlog.py
new file mode 100644
index 0000000..5e10bbd
--- /dev/null
+++ b/pbgui/widgets/commitlog.py
@@ -0,0 +1,155 @@
+from flexx import flx
+
+class CommitLogTableWidget(flx.Widget):
+
+ commits = flx.ListProp()
+ commit_nodes = flx.ListProp()
+
+ dropdown_map = {}
+
+ def _create_dom(self):
+ self.root.get_commits()
+ return flx.create_element('div', {'class': 'table-responsive'})
+
+ def _render_dom(self):
+ return flx.create_element('div', {'class': 'table-responsive'},
+ flx.create_element('table', {'class': 'table table-dark table-hover table-borderless'},
+ flx.create_element('thead', None,
+ flx.create_element('tr', {'class': 'text-muted'},
+ flx.create_element('th', {"scope": "col"}, 'COMMIT'),
+ flx.create_element('th', {"scope": "col"}, 'TIME'),
+ flx.create_element('th', {"scope": "col"}, 'AUTHOR'),
+ flx.create_element('th', {"scope": "col"}, 'MESSAGE')
+ )
+ ),
+ flx.create_element('tbody', None, self.commit_nodes)
+ )
+ )
+
+ @flx.reaction('commits')
+ def on_update_commits(self):
+ global window
+ new_nodes = []
+ current_day = None
+ for commit in self.commits:
+ time = commit.get("time").split(" ")
+
+ date = time[0] + " " + time[1] + " " + time[2] + " " + time[4]
+ if date != current_day:
+ current_day = date
+ date_node = flx.create_element('table', {'class': 'table table-dark table-borderless table-sm m-0 lead'},
+ flx.create_element('tbody', None,
+ flx.create_element('tr', None,
+ flx.create_element('td', None, current_day)
+ )
+ )
+ )
+ new_nodes.append(date_node)
+ new_nodes.append(flx.create_element('hr', {'class': 'bg-light', 'style': 'width:100vw; position:absolute; margin:0'}))
+
+ time = time[3]
+ time_units = time.split(":")
+ hours = int(time_units[0])
+ division = "PM"
+ if hours > 12:
+ hours -= 12
+ elif hours != 12:
+ division = "AM"
+
+ time = hours + ":" + time_units[1] + division
+
+ short_sha = commit.get("sha")[:8]
+
+ author = commit.get("author")
+
+ if author == "ProjectBorealisTeam":
+ author = "Project Borealis"
+
+ author_props = None
+ if author == "Project Borealis":
+ author_props = {"class": "text-warning"}
+ elif author == window.GH_USER:
+ author_props = {"class": "text-info"}
+ author += " (you)"
+
+ row_props = None
+ if commit.get("local"):
+ row_props = {"class": "table-primary"}
+
+ commit_node = flx.create_element('tr', row_props,
+ flx.create_element('td', {"scope": "row", "id": commit.get("sha")}, flx.create_element("span", {"class": "far fa-fw"}), " " + short_sha + " (" + commit.get("human") + ")"),
+ flx.create_element('td', None, time),
+ flx.create_element('td', author_props, author),
+ flx.create_element('td', None,
+ flx.create_element('div', {"class": "row"},
+ flx.create_element('div', {"class": "col-11"},
+ commit.get("message")
+ ),
+ flx.create_element('div', {"class": "col-1"},
+ flx.create_element("div", {"class": "dropdown commit-dropdown"},
+ flx.create_element("button", {"class": "btn btn-outline-light dropdown-toggle", "type": "button", "data-bs-toggle": "dropdown", "id": "dropdown-" + short_sha, "aria-expanded": "false"}),
+ flx.create_element("ul", {"class": "dropdown-menu dropdown-menu-dark", "aria-labelledby": "dropdown-" + short_sha},
+ flx.create_element("li", None,
+ flx.create_element("h6", {"class": "dropdown-header"}, "STATUS")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#", "data-func": "mark_commit", "data-status": "success"}, "Mark as Good")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#", "data-func": "mark_commit", "data-status": "failure"}, "Mark as Bad")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("hr", {"class": "dropdown-divider"})
+ ),
+ flx.create_element("li", None,
+ flx.create_element("h6", {"class": "dropdown-header"}, "VERSIONING")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#"}, "Switch to Version")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#"}, "Request Binaries Build")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("hr", {"class": "dropdown-divider"})
+ ),
+ flx.create_element("li", None,
+ flx.create_element("h6", {"class": "dropdown-header"}, "SOURCE CONTROL")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#"}, "Revert")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#"}, "Copy to Branch")
+ ),
+ flx.create_element("li", None,
+ flx.create_element("hr", {"class": "dropdown-divider"})
+ ),
+ flx.create_element("li", None,
+ flx.create_element("a", {"class": "dropdown-item", "href": "#"}, "View on GitHub")
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ new_nodes.append(commit_node)
+
+ for node in self.commit_nodes:
+ node.dispose()
+
+ self.update_commit_nodes(new_nodes)
+
+ @flx.reaction('commit_nodes')
+ def on_update_commit_nodes(self):
+ global window
+ window.post_commit_update(self.outernode)
+
+ @flx.action
+ def update_commits(self, commits):
+ self._mutate_commits(commits)
+
+ @flx.action
+ def update_commit_nodes(self, commit_nodes):
+ self._mutate_commit_nodes(commit_nodes)
diff --git a/pbgui/widgets/settings.py b/pbgui/widgets/settings.py
new file mode 100644
index 0000000..aa35959
--- /dev/null
+++ b/pbgui/widgets/settings.py
@@ -0,0 +1,44 @@
+from flexx import flx
+
+class SettingsWidget(flx.Widget):
+
+ def init(self):
+ with flx.VBox():
+ with flx.Widget():
+ with flx.Widget(css_class="row"):
+ with flx.Widget(css_class="col-2"):
+ flx.Label(text='UE Folder')
+ with flx.Widget(css_class="col-4"):
+ self.download = flx.LineEdit(placeholder_text="Folder to download Unreal Engine to", minsize=(400, 28))
+ with flx.Widget():
+ with flx.Widget(css_class="row"):
+ with flx.Widget(css_class="col-2"):
+ flx.Label(text='Project Version')
+ with flx.Widget(css_class="col-4"):
+ self.version = flx.LineEdit(text="latest")
+ with flx.Widget(css_class="row"):
+ self.symbols = flx.CheckBox(text="Download Symbols")
+ self.autosync = flx.CheckBox(text="Always Auto-Sync")
+ self.autosync = flx.CheckBox(text="Clean Up Old Engine Versions")
+ with flx.GroupWidget(title="Engine Bundle"):
+ with flx.VBox():
+ self.bundle_editor = flx.RadioButton(text="Editor (development only)", checked=True)
+ self.bundle_engine = flx.RadioButton(text="Engine (can package game)")
+ with flx.GroupWidget(title="Launch App"):
+ with flx.VBox():
+ self.launch_editor = flx.RadioButton(text="Unreal Editor", checked=True)
+ self.launch_vs = flx.RadioButton(text="Visual Studio")
+ self.launch_rider = flx.RadioButton(text="Rider")
+ self.launch_non = flx.RadioButton(text="None")
+ with flx.GroupWidget(title="Download Binaries"):
+ with flx.VBox():
+ self.bundle_editor = flx.RadioButton(text="If Promoted", checked=True)
+ self.bundle_engine = flx.RadioButton(text="Always")
+ self.bundle_engine = flx.RadioButton(text="Never")
+ with flx.Widget(css_class="row"):
+ with flx.Widget(css_class="col"):
+ flx.Label(text='Git exe')
+ self.exe_git = flx.LineEdit(placeholder_text="Custom Git exe", minsize=(500, 28))
+ with flx.Widget(css_class="col"):
+ flx.Label(text='Git LFS exe')
+ self.exe_git = flx.LineEdit(placeholder_text="Custom Git LFS exe", minsize=(500, 28))
diff --git a/pbpy/pbdispatch.py b/pbpy/pbdispatch.py
index de4162e..0b2f26e 100644
--- a/pbpy/pbdispatch.py
+++ b/pbpy/pbdispatch.py
@@ -30,7 +30,6 @@ def push_build(branch_type, dispath_exec_path, dispatch_config, dispatch_stagedi
return False
# Push and Publish the build
- proc = pbtools.run_with_combined_output([dispath_exec_path, "build", "push", branch_id, dispatch_config, dispatch_stagedir, "-p"])
- pblog.info(proc.stdout)
+ proc = pbtools.run([dispath_exec_path, "build", "push", branch_id, dispatch_config, dispatch_stagedir, "-p"])
result = proc.returncode
return result == 0
diff --git a/pbpy/pbgit.py b/pbpy/pbgit.py
index f532f3c..9a2cb62 100644
--- a/pbpy/pbgit.py
+++ b/pbpy/pbgit.py
@@ -26,6 +26,16 @@ def compare_with_current_branch_name(compared_branch):
return get_current_branch_name() == compared_branch
+@lru_cache
+def is_on_expected_branch():
+ binaries_mode = pbconfig.get_user("project", "binaries", "on")
+ if binaries_mode == "force":
+ return True
+ elif binaries_mode == "local":
+ return False
+ return compare_with_current_branch_name(pbconfig.get("expected_branch_name"))
+
+
@lru_cache()
def get_git_executable():
return pbconfig.get_user("paths", "git", "git")
@@ -173,6 +183,10 @@ def stash_pop():
pbtools.error_state(f"git stash pop failed due to an unknown error. Request help in {pbconfig.get('support_channel')} to resolve possible conflicts, and please do not run UpdateProject until the issue is resolved.", True)
+def get_remote_url():
+ return pbconfig.get("git_url")
+
+
def check_remote_connection():
current_url = pbtools.get_one_line_output([get_git_executable(), "remote", "get-url", "origin"])
recent_url = pbconfig.get("git_url")
@@ -260,3 +274,8 @@ def get_credentials():
def get_modified_files():
proc = pbtools.run_with_output([get_git_executable(), "status", "--porcelain"])
return [pathlib.Path(line[3:]) for line in proc.stdout.splitlines()]
+
+
+def get_commits():
+ proc = pbtools.run_with_combined_output([get_git_executable(), "log", "-100", "--no-merges", f"origin/{get_current_branch_name()}"])
+ return proc.stdout
diff --git a/pbpy/pblfs.py b/pbpy/pblfs.py
new file mode 100644
index 0000000..b46691f
--- /dev/null
+++ b/pbpy/pblfs.py
@@ -0,0 +1,12 @@
+from pathlib import Path
+
+def object_dir():
+ return Path(".git/lfs")
+
+def local_object_dir(oid: str):
+ return Path(object_dir(), oid[0:2], oid[2:4])
+
+def object_path(oid: str):
+ return local_object_dir(oid) / oid
+
+
diff --git a/pbpy/pbtools.py b/pbpy/pbtools.py
index 6a19102..8cd73ce 100644
--- a/pbpy/pbtools.py
+++ b/pbpy/pbtools.py
@@ -72,7 +72,32 @@ def run_with_output(cmd, env=None, env_out=None):
return proc
-def run_stream(cmd, env=None):
+def default_stream_log(msg):
+ pblog.info(str)
+
+
+def checked_stream_log(msg, error="error", warning="warning"):
+ if error in msg:
+ pblog.error(msg)
+ elif warning in msg:
+ pblog.warning(msg)
+ else:
+ pblog.info(msg)
+
+
+def raised_stream_log(msg, error="error", warning="warning"):
+ if error in msg:
+ pblog.error(msg)
+ elif warning in msg:
+ pblog.warning(msg)
+ else:
+ print(msg)
+
+
+def run_stream(cmd, env=None, logfunc=None):
+ if logfunc is None:
+ logfunc = default_stream_log
+
if os.name == "posix":
cmd = " ".join(cmd) if isinstance(cmd, list) else cmd
@@ -82,7 +107,7 @@ def run_stream(cmd, env=None):
# TODO: handle encoding
try:
for line in iter(lambda: proc.stdout.readline(), ''):
- pblog.info(line)
+ logfunc(line)
except:
continue
returncode = proc.poll()
@@ -290,6 +315,7 @@ def error_state(msg=None, fatal_error=False, hush=False, term=False):
if fatal_error:
# Log status for more information during tech support
pblog.info(run_with_combined_output([pbgit.get_git_executable(), "status"]).stdout)
+ pblog.info(run_with_combined_output([pbgit.get_git_executable(), "reflog", "-10"]).stdout)
# This is a fatal error, so do not let user run PBSync until issue is fixed
with open(error_file, 'w') as error_state_file:
error_state_file.write("1")
@@ -341,11 +367,12 @@ def maintain_repo():
f"{pbgit.get_lfs_executable()} dedup"
]
- if os.name == "nt":
+ if os.name == "nt" and pbgit.get_git_executable() == "git":
proc = run_with_combined_output(["schtasks" "/query", "/TN", "Git for Windows Updater"])
# if exists
if proc.returncode == 0:
cmdline = ["schtasks", "/delete", "/F", "/TN", "\"Git for Windows Updater\""]
+ pblog.info("Requesting admin permission to delete the Git for Windows Updater...")
if not pbuac.isUserAdmin():
pbuac.runAsAdmin(cmdline)
else:
@@ -361,16 +388,36 @@ def maintain_repo():
if is_shallow == "true":
pblog.info("Shallow clone detected. PBSync will fill in history in the background.")
commands.insert(0, f"{pbgit.get_git_executable()} fetch --unshallow")
+ else:
+ # repo was already fetched in UpdateProject for the current branch
+ current_branch = pbgit.get_current_branch_name()
+ fetch_base = [pbgit.get_git_executable(), "fetch", "--no-tags", "origin"]
+ # sync other branches, but we already synced our own in UpdateProject.bat
+ configured_branches = pbconfig.get("branches")
+ branches = []
+ if configured_branches:
+ for branch in configured_branches:
+ if branch == current_branch:
+ continue
+ branches.append(branch)
+ fetch_base.extend(branches)
+ commands.insert(0, " ".join(fetch_base))
run_non_blocking(*commands)
lfs_fetch_thread = None
+lfs_fetch_should_print = False
+
+
+def lfs_fetch_log_func(msg):
+ if lfs_fetch_should_print:
+ print(msg)
def do_lfs_fetch():
branch_name = pbgit.get_current_branch_name()
- run_with_combined_output([pbgit.get_lfs_executable(), "fetch", "origin", f"origin/{branch_name}"])
+ run_stream([pbgit.get_lfs_executable(), "fetch", "origin", f"origin/{branch_name}"], logfunc=lfs_fetch_log_func)
def start_lfs_fetch():
@@ -382,11 +429,18 @@ def start_lfs_fetch():
def finish_lfs_fetch():
pblog.info("Finishing LFS fetch...")
+ global lfs_fetch_should_print
+ lfs_fetch_should_print = True
lfs_fetch_thread.join()
pblog.info("Finished LFS fetch.")
def resolve_conflicts_and_pull(retry_count=0, max_retries=1):
+ branch_name = pbgit.get_current_branch_name()
+ if branch_name not in pbconfig.get("branches"):
+ pblog.info(f"Branch {branch_name} is not an auto-synced branch. Skipping pull.")
+ return
+
def should_attempt_auto_resolve():
return retry_count <= max_retries
@@ -399,18 +453,35 @@ def should_attempt_auto_resolve():
if not it_has_any(out, "-0"):
start_lfs_fetch()
pbunreal.ensure_ue_closed()
- pblog.info("Please wait while getting the latest changes from the repository. It may take a while...")
- # Make sure upstream is tracked correctly
- branch_name = pbgit.get_current_branch_name()
- pbgit.set_tracking_information(branch_name)
- pblog.info("Rebasing workspace with the latest changes from the repository...")
+ pblog.info("Please wait while getting the latest changes from the repository. It may take a while...")
+
# Get the latest files, but skip smudge so we can super charge a LFS pull as one batch
- result = run_with_combined_output([pbgit.get_git_executable(), "-c", "filter.lfs.smudge=", "-c", "filter.lfs.process=", "-c", "filter.lfs.required=false", "rebase", "--autostash", f"origin/{branch_name}"])
+ cmdline = [pbgit.get_git_executable(), "-c", "filter.lfs.smudge=", "-c", "filter.lfs.process=", "-c", "filter.lfs.required=false"]
+ # if we can fast forward merge, do that instead of a rebase (faster, safer)
+ if it_has_any(out, "+0"):
+ pblog.info("Fast forwarding workspace to the latest changes from the repository...")
+ cmdline.extend(["merge", "--ff-only"])
+ else:
+ pblog.info("Rebasing workspace with the latest changes from the repository...")
+ cmdline.extend(["rebase", "--autostash"])
+ cmdline.append(f"origin/{branch_name}")
+ result = run_with_combined_output(cmdline)
# Checkout LFS in one go since we skipped smudge and fetched in the background
finish_lfs_fetch()
- run_with_combined_output([pbgit.get_lfs_executable(), "checkout"])
+ diff_proc = run_with_combined_output([pbgit.get_git_executable(), "diff", "--name-only", f"HEAD...origin/{branch_name}"])
+ if diff_proc.returncode == 0:
+ changed_files = diff_proc.stdout.splitlines()
+ else:
+ changed_files = []
+ # if there's a lot of files, or we didn't find any, just let LFS do the work
+ lfs_checkout = [pbgit.get_lfs_executable(), "checkout"]
+ if len(changed_files) <= 50 and changed_files:
+ lfs_checkout.extend(" ".join(changed_files))
+ run(lfs_checkout)
+ else:
+ run_with_combined_output(lfs_checkout)
# update plugin submodules
- run_with_combined_output([pbgit.get_git_executable(), "submodule", "update", "--init", "--", "Plugins"])
+ run([pbgit.get_git_executable(), "submodule", "update", "--init", "--", "Plugins"])
code = result.returncode
out = result.stdout
pblog.info(out)
@@ -427,8 +498,6 @@ def handle_error(msg=None):
if not error:
handle_success()
- elif "fast-forwarded" in out:
- handle_success()
elif "up to date" in out:
handle_success()
elif "rewinding head" in out and not it_has_any(out, "error", "conflict"):
diff --git a/pbpy/pbunreal.py b/pbpy/pbunreal.py
index 6af1418..4a4724d 100644
--- a/pbpy/pbunreal.py
+++ b/pbpy/pbunreal.py
@@ -626,21 +626,13 @@ def download_engine(bundle_name=None, download_symbols=False):
pblog.info("Registering Unreal Engine file associations")
selector_path = get_unreal_version_selector_path()
cmdline = [selector_path, "/fileassociations"]
+ pblog.info("Requesting admin permission to isntall Unreal Engine Prerequisites...")
if not pbuac.isUserAdmin():
pbuac.runAsAdmin(cmdline)
else:
pbtools.run(cmdline)
- # work around bug
- build_version = base_path / Path("Engine/Build/Build.version")
- with open(str(build_version), "w") as f:
- build_version = ""
- for line in f.readlines():
- if not pbtools.it_has_any(line, "BuildId", "BuildVersion"):
- build_version += line
- f.write(build_version)
- # generate project files for developers
- is_on_expected_branch = pbgit.compare_with_current_branch_name(pbconfig.get('expected_branch_name'))
- if not is_on_expected_branch:
+ # generate project files for developers)
+ if not pbgit.is_on_expected_branch():
uproject = str(get_uproject_path())
pbtools.run([selector_path, "/projectfiles", uproject])
@@ -805,7 +797,7 @@ def build_source():
if ms_build is None:
pbtools.error_state("Could not find MSBuild.")
sln_path = get_sln_path().resolve()
- proc = pbtools.run_stream([ms_build, str(sln_path), "/nologo", "/t:build", '/property:configuration=Development Editor', "/property:Platform=Win64"])
+ proc = pbtools.run_stream([ms_build, str(sln_path), "-m", "/nologo", "/t:build", '/property:configuration=Development Editor', "/property:Platform=Win64"], logfunc=lambda x: pbtools.checked_stream_log(x, error="error ", warning="warning "))
if proc.returncode:
pbtools.error_state("Build failed.")
@@ -813,7 +805,7 @@ def build_source():
def build_game(configuration="Shipping"):
base = get_engine_base_path()
uat_path = base / "Engine" / "Build" / "BatchFiles" / "RunUAT.bat"
- proc = pbtools.run_stream([uat_path, "BuildCookRun", f"-project={str(get_uproject_path())}", f"-clientconfig={configuration}", "-NoP4", "-NoCodeSign", "-cook", "-build", "-stage", "-prereqs", "-pak", "-CrashReporter"])
+ proc = pbtools.run_stream([uat_path, "BuildCookRun", f"-project={str(get_uproject_path())}", f"-clientconfig={configuration}", "-NoP4", "-NoCodeSign", "-cook", "-build", "-stage", "-prereqs", "-pak", "-CrashReporter"], logfunc=lambda x: pbtools.checked_stream_log(x, error="Error: ", warning="Warning: "))
if proc.returncode:
pbtools.error_state("Build failed.")
diff --git a/pbsync/__main__.py b/pbsync/__main__.py
index 9453f24..d3ef582 100644
--- a/pbsync/__main__.py
+++ b/pbsync/__main__.py
@@ -19,6 +19,9 @@
from pbpy import pbdispatch
from pbpy import pbuac
+import pbgui.main
+import pbgui.gateway
+
try:
import pbsync_version
except ImportError:
@@ -34,7 +37,6 @@ def config_handler(config_var, config_parser_func):
def sync_handler(sync_val: str, repository_val=None, requested_bundle_name=None):
-
sync_val = sync_val.lower()
if sync_val == "all" or sync_val == "force" or sync_val == "partial":
@@ -112,7 +114,7 @@ def sync_handler(sync_val: str, repository_val=None, requested_bundle_name=None)
bundled_git_lfs = True
if not is_admin and len(delete_paths) > 0:
- pblog.info("Requesting permission to delete bundled Git LFS which is overriding your installed version...")
+ pblog.info("Requesting admin permission to delete bundled Git LFS which is overriding your installed version...")
quoted_paths = [f'"{path}"' for path in delete_paths]
delete_cmdline = ["cmd.exe", "/c", "DEL", "/q", "/f"] + quoted_paths
try:
@@ -195,26 +197,13 @@ def sync_handler(sync_val: str, repository_val=None, requested_bundle_name=None)
fatal_error=True)
current_branch = pbgit.get_current_branch_name()
- expected_branch = pbconfig.get('expected_branch_name')
- is_on_expected_branch = current_branch == expected_branch
# undo single branch clone
if not is_ci:
pbtools.run([pbgit.get_git_executable(), "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"])
- # repo was already fetched in UpdateProject for the expected branch, so do it here only for dev
- if not partial_sync and not is_on_expected_branch:
- pblog.info("Fetching recent changes on the repository...")
- fetch_base = [pbgit.get_git_executable(), "fetch", "origin"]
- branches = {expected_branch, current_branch}
- branches.update(pbconfig.get('branches'))
- fetch_base.extend(branches)
- pbtools.get_combined_output(fetch_base)
-
- pblog.info("------------------")
-
# Execute synchronization part of script if we're on the expected branch, or force sync is enabled
- if sync_val == "force" or is_on_expected_branch:
+ if sync_val == "force" or pbgit.is_on_expected_branch():
if partial_sync:
pbtools.maintain_repo()
else:
@@ -277,10 +266,10 @@ def sync_handler(sync_val: str, repository_val=None, requested_bundle_name=None)
else:
error_state(f"Something went wrong while registering engine build {bundle_name}-{engine_version}. Please request help in {pbconfig.get('support_channel')}.")
- # Clean old engine installations, do that only in expected branch
- if is_on_expected_branch:
+ # Clean old engine installations
+ if pbconfig.get_user("ue4v-user", "clean", True):
if pbunreal.clean_old_engine_installations():
- pblog.info("Old engine installations are successfully cleaned")
+ pblog.info("Successfully cleaned old engine installations.")
else:
pblog.warning("Something went wrong while cleaning old engine installations. You may want to clean them manually.")
@@ -442,6 +431,7 @@ def main(argv):
parser.add_argument("--sync", help="Main command for the PBSync, synchronizes the project with latest changes from the repo, and does some housekeeping",
choices=["all", "partial", "binaries", "engineversion", "engine", "force", "ddc"])
+ parser.add_argument("--gui", help="Open the GUI app", action='store_true')
parser.add_argument("--printversion", help="Prints requested version information into console.",
choices=["current-engine", "latest-engine", "project"])
parser.add_argument(
@@ -485,7 +475,7 @@ def pbsync_config_parser_func(root):
'gcm_download_suffix': ('git/gcmsuffix', None),
'expected_branch_name': ('git/expectedbranch', None if args.debugbranch is None else str(args.debugbranch)),
'git_url': ('git/url', None),
- 'branches': ('git/branches', None),
+ 'branches': ('git/branches/branch', None),
'checksum_file': ('git/checksumfile', None),
'log_file_path': ('log/file', None),
'user_config': ('project/userconfig', None),
@@ -541,7 +531,12 @@ def pbsync_config_parser_func(root):
run UpdateProject again.""", True)
# Parse args
- if not (args.sync is None):
+ if not (args.gui is None):
+ def sync():
+ return sync_handler(args.sync, args.repository, args.bundle)
+ pbgui.set_default_page("sync")
+ pbgui.main.run(sync)
+ elif not (args.sync is None):
sync_handler(args.sync, args.repository, args.bundle)
elif not (args.printversion is None):
printversion_handler(args.printversion, args.repository)
diff --git a/requirements-linux.txt b/requirements-linux.txt
index cdb5908..8d7ead4 100644
--- a/requirements-linux.txt
+++ b/requirements-linux.txt
@@ -5,4 +5,5 @@ verboselogs
gsutil
crcmod
pyinstaller
+https://github.com/flexxui/flexx/archive/master.zip
tinyaes
diff --git a/requirements.txt b/requirements.txt
index 9d9d113..05220ba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,8 @@ verboselogs
gsutil
crcmod
pywin32
+pscript
+https://github.com/flexxui/flexx/archive/master.zip
# pyinstaller
pefile