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 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 @@ + + + + +Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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