This commit is contained in:
Nick Beckley 2026-06-01 11:20:24 +01:00
parent 39c72c3b38
commit cb7405c0e8
46 changed files with 1187 additions and 126 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFF27qRbxog94ozigWoqUWFLVpYOx3L6jdInFvuQkDveHjb97wjUR/RNKR+9PplzPGvJCa5HErCSYMZYYyfmBtp3SnVaM1Z8aIv0lNxif8RjpiVQg8OKQSDVIFTNNeDoECoOOMAxNalZ7+80K6N+LHBvA8Bmzm5U9MyzhH0pG/xgjz13K2H5swzSs0aZ5wF5fsYyhm/tg9PNHf7/eH5/31rwWXIm5GIB1BUINzwdEh9J6qngvaKmkoE0y5oHpttbvKxpgxRT81C3z5fj7MOWIOnWiEbBgVjlxqAj77Yrn3Cx6+nYqLDdFjqddbefvs3sgZE+lE/R/Ma7u7MI13y9pPeMbp5NOC6S7Mx6OPQGpySvMJUx5xw3LG1M/LmAvREKxvW86h9VZpC63UQlqQDKFlVoMD540ygwhaD9C3zqpBgeAKDCrVW1KTDdw0+TzOsbp5f5hGjJnU5Pml2k8+D3M6vkP/ciW6+wmgxTbpiGRaGmtRSSm9M71gCNYEY0QfPLNDL5AHZNop67hlXDrBnNTGthY8c4HU5IMPr2PEavdIOsI4CpAt0kFwQ1ULnnotHTVGmgF5cFZwUpOHOeYxrvO6VLuY8XPa7ks68qufFrze9D6F1zFjyGsqED6ZEsZ9mmENufoeAM4a1kjGimjNpzVXHxHGhCFjwXFI663f/yTCiE30sKaEMVd7n1+r3/zySjrifBu4d955b0IYUDMcGDBvmAU0YBCUAAvKKTDKsWCU77kCr3k/lAqpCZa8VVvEpfZMDcFSBBRUSSGoM85Q21vVS66Z0/07jtXzVHQ+tEwhSOpwAKoc99QpKSlI7a0wg7Vi+AHJLSycFcCA5+uybg4MQItgOBWKa6qc47R12tLetr3rTYs6FM9PmCJOP4D6MIY0L/OQq1vWqnfoPq6xPDjVlruvY6T/bDN6nMMXTFWZsR9j2a/bWC81OaJf1oTH7b3q3si+LFd12J4toRvWaleTl+xTXk+3sjRNy5W6XC6XfwEAAP//AwBRE7MW9AQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadEbxRnFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CsE+E/Rwezq9e6GQFigh+a0kudSMa8bFkXe9avqmqVrdSNmZf6EEf0/foYdWOSecIVaTJqaQN2yoR8cQyQgukMtG5Z72ywL9K1hfIb14R9W8fKa4+DlAD3/7gPPXPNv6yk3zilWcJ9qHJdngMpkP+909++v2cHw4XF4ttGRw5RF6IFSjMc6w0Q2WqUFI1qlaMy65Mk4NTduYC8yHRDHYqVrw6W0+zilQcr2sZF1xJg2cS0CbbKY82IWO30+ZxWbRXa6XW3m79q/wQhF6Wf4089Luxk3+5nkZVj/hzeVMdnnKKk9xPlFMnjZPNpnTZJOfQ3GdtJs8hQQlPHwqDpNN4xyffzPq00X//n/IP+bp776RWxPhbl5Dgh6EqqGE99Y9+kDF/g564IIk1h2xUQrNVIeW2aY2TOtajyScaaWAEnZzSD6s87oU+5DoS9wEQA9/2GmhC/Xb6B59IpfWmFX9o/MuHOKMq0vFm/+CV7yqOc+gNZ3WVHwk9JFcoqzvGNdrv/9ApJYbaLfGSCEVB5seiz/t8gg9GNs5YY011mrnRmo4jRy51bxF0qgJlcQWlVGoleFOKzsIhbYRw5grUALluIttw4xsbK1oYLWqiSmjLRsai2xUynU06lFhpvjmY/EwZZx1HVeENTM0IlNGWGZUXTOsG9tKPbatHH+x5Jq+4NlgpJfLX9kYaMSOUAsmlWiYMkawzjQtG9puMIPuqHGZ8z3FQNMvRr33Ls7LPKbiujzFb9Z9XEP+78W2SF99YN+2jO5m90SxyBlbH/J6X2M9n8/nHwAAAP//AwCu1fIaNgQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUUW/cKBDHv4o1z8YBDNj4Ldq0ulWul1W76lV9wzBuUBy8Apw2ivLdT3j3lObpdC8IjZnhP7/54xcI5hFhgOvT6cMThpyghuy3EKdcEaoIZUfaD0IOUjadarns6Heowd/iMwzQCWuZ1UhaVEiEo5KM7WSJc6gZZY5yKUpN8yPB8ALGNw6fvMVmSV8xJr8EGOBvH9zys9xtfGPnZXVNXGbch5RNsEXMX/vdLfnz+nC8O5xPJUwlufEOBkAnJq2tJpMdDREj46QXrSKUU6GtGGUn9TnNh4wxmLlJ7uHtfrfkgNkOvOFtQwnX8FqDM9kUyaNJeHw+FRUbopsSr7fwth1e4AkjDLz+F+a53JWd/dVjGlc/u6vzmk16SNs6GZuX+Aw1nOJywpg9bnyuU8LHcX4+mvTw8XymbNOHX2jXjG63rCHDAExQqGEDNM8m+yVUF4272WPI28ccfUje/letFmq4+1IdZpOnJT6+m8eXM+b9/6D8ydh7H7Da38AAlCF3bY9k4kwR0TtDjGw1UapVEzKrO86ght0Ssg/rsqZqHzL+iFtLMMBHMyc867uO9t5ntHmNpc9vqvjqEBe32ly9zZLRhjYtLXju1nxac/UZnY9oM5YmjnG91Pv6PoUrviXt1hgx5Opg8n31h0n3MIA2vWVGG22MsnZCSXGijhpFO4fKKXSCu84JLZwSmlolzMiEM5KNU4lADVisU13cagRDKoidOBIhu5b0rZBEIleCtd2IujB541jdzSXP2J4KdC3RODkiNDNEi7YlrpWm42rqOj79huTiB0ZLMYdP53e3KVDO9egUI1wwSYTWjPRadmTs+lGPqkdpi+ZbjAHn30B98jYuaZlydXFI9Q7d5zWUf0e1ueWnD+TXNqObxT5grMqMjQ/lqVzG+vr6+voPAAAA//8DAEU35m+CBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwY7cKBD9FYuz8QDG2Pg26tlVWpPstJJREuWGqSKN4oEWxpNEo/73Fe6OJtnb3tCDV/XqveKFBPOEZCS3p9NfzxjyQmqS/QYJJhRlijL+yIZRdmPXNb3qpeT8C6mJv8efZCS9tJZbjbRFhVQC6+jUOksBUHPGgYlOlprm60LGF2J8A/jsLTZx+Yhp8TGQkXzyAeL30tv4xs5xhSbFGfdhySbYIuaf/e6evr09PD4cLq8WXAq58UBGgiCd1lZTZydD5cQFHWSrKBNMaiunru/0heZDxhTM3Czw7bU/xBww21E0om0YFZqcawImmyJ5Mgs+/jwVFZtFdwWvN3g7ji/kGRMZRf3LzEu5Gzv7GxufnkyAG+eDX46kJqcUT5iyx82ObcJ5NtnHUF2b7GaPIZOaPHyoDrPJLqanPzz6cBl9/z8mf2fs0Qes9ndkJIyjgHZA6gRXVA5gqOlaTZVqlUNudS84qckuhuzDGtel2oeMX9OmkozkbzMveNF3m+zRZ7R5TUX6Z1Wyxh8+7yIUgJGaHFKE1ebq1W7OGta0rFw+rPm05uo9gk9oM5aZHtN6Lf8filBiI+3WlDDk6mDysXpjliMZiTaD5UYbbYyy1mHH0DFgRrEeUIFCkAJ6kFqCkppZJc3EJZiOT64gRXdJt9oWSnVMOdVJ2pvWUolOUdNDT7mQEzrVT0wXi15trR7mwjN2YBKhpRodUKm5oVq2LYW2M71Qru+F+82Sa+KclWKAz5evcVEAMCAoToXkHZVaczrorqdTP0x6UgN2tmi+xxRw/s2od96muESXq+vCVH9Y934N5XtX2/J894H+2CK7i/YbpqpEbnwo23xN+Xw+n/8FAAD//wMAC1T6KCUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/jNhD9KwKvFRWSoihRt9TJokaaxFgbbdHbiBzGbGTKoKhkF0H+eyE52XwsCvQiCENy3ps3b+aJBDggacn58Xj5gCGNJCfJLyHBhKJMUcZ3rGll1VZVUau6akr+N8mJv8LvpCW1NIYbjbREhVRaVtGudIZai5ozbpmo5JwT7kbSPhHwhcUHb7AYxj8wjn4IpCV/+mCHxxkbfGH6YbJFHHpchzFBMDOZm/Xqiv5+vtndbk63Rhznx4W3pCVopdPaaOpMB1R2XNBGlooywaQ2sqvqSp+e+ZAwBuiL0d6/4dshBUymFYUoC0aFJs85sZBgptzBiLvvx5nFItHFHM+X8PLbPpEHjKQV+auYp3Rnpvdnh7GbfG/Pli/JyTEOR4zJ46LG7TZ7I8FZwQqhBGMkJ9dg9j5gtr7Ibvu5RDANk2hLqtFZKjUHqmVZUltWUAvl6lo4kpMrjAH7d1mvvYnDOLiUvYicfcDZnq8uA3Q9zhhfoB+R5GQ9bhMEC/0Q8PIbmimdcu3iNB+vhpB8mIZpzNYh4V2El/PX96spRgwp20DaZ7/BuCct0dAYDho0gDLGYcXQMctAsdqisgqtFLa2UkurpGZGSei4tFDxzs0RkpNfZw23kzE4jm9sFpP0/cIhe+nTqvcY0gcZSUsYR2HLBqkTXFHZWKBQlZoqVSqH3Oha8FeYHcQ7TKQlrum6jhnTCam5AaGhtI6VVjktRF2DdGVVS6FUxWowtq4Fl7as0UDpStYIkpNNHOxk0udWl0sDNnH4B02apSItUayqGgNdJ5TkDegGJVeyURprLR0wWZoOeNVA13EGFTO6EQa40iA7W7kfKl2GOx/wwo/HHr6/w20KVZS/GICGO5Cfrn+JcMDHId7fnGxc3FzuSE6+TmHeCNl6tsijD/Sbmif6euqT3+0jgkV7PVj82Uc4j0u2TKjmtdVOM4qWSyqbzlGw3FEHElinWKNxtu/FYO4xZrPDwId5qn4ku91m59HsfUKTpjjz+2vhcbvNNj0kN8TDh03yrrDPAhSi5GzujMWH0zZaOCprG7SKUyF5RaXWnDa6qmlXN53uVIOVmRW7ndJxStlXtD6iSUvFL15cMFd7NPc/i/HqgheLcjZ7bXvaYou0/2uJPefkgDBOEQ/Ltm6fyDoEjAvyxXQaxXW49n3vRzRDsCNpOde8aGRdvjD8z3tClkWtuHh+fn7+FwAA//8DANjfCysbBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSU227jNhCGX0XgtanwJOpwlzpd1MhmE2zcbdG7ETlyiMikQVHZBoHffUHZQZJd9KJ3woj/HL75yRfiYY+kI5eHw+9P6NNEViS5JSSY0JRpyviWNZ2qOylKqWvesvofsiLuGp9JR2plDDctUokaqbKsor0cDLUWW864ZaJSOSfsJtK9EHClxSdnsAzTN4yTC5505C/nbfiea4MrzRhmW8Yw4sZPCbzJzXzZrK/p58u77e3d6dSEUxaXzpKONLW0gI2ifdNrqoSwFKpaUqWhByt4y8RwkjmfMHoYy8k+vtW3IXlMphOlkCWjoiXHFbGQILfcw4Tb50PuYkF0leOrJbx8di/kCSPpxOoV5indhRndxX7qZzfaizHsds7v1sEPbjdHSLnwihxiOGBMDhc4vzkP8flz2O1yPvIJxgnJimwx7p2H8fTjCgeYx3Qf5riQmezjf50hHYE5BbIiC8lxXMoW52HWo0OfftG+K3x7X9yNkIYQ9x+WdH9iv/kf6G/APDiPxeaKdIRxFFY2SAfBNVWNBQqVbKnWUg/ITVsLTlZkHXxyfg7zVGx8wt2Z2of+LqN5cAlNmmOe6W+dzXYXg51NKt4WzFnJSslYFs3pMKfiK1oX0STMQ2zjfM73k0RosYg+uRF/wbOeY0SfijtID8UfMD2QjrTQGA4ttADamAErhgOzDDSrLWqr0Spha6taZbVqmdEKeq4sVLwfcoSsCGaTFYuveyWlRcGoZBapQq5oLxqgrK4VykHLumo+wC1ux6wD0zCFVtIWB0tVy4G2SkpqZQW10ENdL0t55XQ2BGeZ+nsT/jmhPXvpy8nar7NbfDrd46VPbW2DVnMqFK+oaltOm7aqaV83fdvrBiuTJ1sHP4VXkN8w9mFyKT8iN867PYw/H3nbzDVGj+O77dw4E8MUhlScbVl82NfX2edXrFgs+t15+u9ijKtgHjEW2Vjg/LtlHo/H4w8AAAD//wMAtEuIEAwFAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaP5eO/NG/5AvdoDatDycPh0hD4OaIGim0KMMIGJwIRuSd3wqilYXknJKRH/ogVyd/CGGlRxramWgAsQgLkhJW4Lq7ExICmhhrCSp57qeUDND6RcbuDoNOR++AvC4HyPGvS3641/TbOVy3XnR5MH38G6H6LqdQLzdb26w1+Wm+3D5pQ1wJCKc2dQg+qqMApqjtu6FZgzZrAqqwJzoVplGJWE2VOZ6yOEXnX5YF4u842PPUTdsJwVOcFMovcFMiqqBLlVA2zfDgnFJNFtii+m8PTZ/EBHCKhhi7OYp3Y3unM3+6EdXWduogrPEG1Qe3j14QWOqkMLdAj+ACE6mLR5eMoukCjJSc4EIwQt0L3SO9dDtr7NHrpEWOmacDAFlmAN5pIqLHlRYFOUqmLCVtVE+A5CD91V13ungx+8jdksefZhzsr30fWjH4ds3Ud4Diqe6j6rbgC0QJux7dywewRl3rb+cUz/+rFLXFZjCNDHbKPiLvtTDTvUIKlqTZVUUimhtYWSgCWGKEEqA8IIMJyZynDJjeCSaMFVS7lRJW1tiqAFehjjYYxn/b9fgdgGt9+DuSDYThpvOhWtD/u1gT4669JqzhmTobpuYpXNPVedgz5+EBk1iFBgpqgBW0YF5rVRWJWFxEIUwgLVsmL0JJh1z+NPnYi1hhe2rGvRypYSYLaqOalsS4y2hZambKFiFalqqKUmpC05rVupiQFacpPYBW9GHX+1QjEt6HHs03Vm60T71fX4u+AXRTbBR699lxbmOnh6GyLsf7e1ld8f/OBi4m/n1X4b4N736e98/7Nmc/HSx0sM0h1k0+kVlNu2rUosJOeYU8lxq2uNZWEtr4RlYBLwW69fIGTJYMr1007Onnp4ypZB71wEHceQJv8zkTqt8/P5ZC565F8/bVc+wPJwWMzRP45JpFOzswE+vCvfhpS+80O8ojxTe4LOnoFd2cnA8fRUuRQUxtRgBMWM0xJzKSmuZVnhtqpb2YoaSn2xa/YIxgXQceq3DWOaNiv7O18uQ3RW6Tik4/ni9WSn2Z9zyq8gr9b2s/o0/Gp1s5XmTpQkyz6dHs7JQf/r3Xx/f3//DwAA//8DAMe2UmsoBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS/zQzbBb1HVTL+JFWvRGcka2EK20oKhNA2P/e0DtBol7600Y6J0ZPvPOvJHJH5B05PZ4/OWEU15ITfKwhQQTmjJNGd8z2ynTSdEIyxxX5h9Sk+ERv5KOGBUjjw6pRI1UAWtpkH2kAOg448BEq0pO/7KQ7o34oQE8DRGbefmEaRnmiXTkr2GC+Uup7YcmjvMKTZpHfJiW7KdYmvnz4e6R/nG72z/tLn8tuBRxMwDpiDUSPFpFgw2aKiGA+tZIqrQPHgR3TPQX2TBlTJMfmwU+/6gPc54wx040QjaMCkfONQGffWk5+AX3X4+liw3RfYnXW3j77N7ICRPpRP0d5iXdTRyHmzwfRzzhePRpwXQT58PBT0BqckzzEVMecMOyvXQcfR7mqboWuxsHnDKpydNztRt97ud0eMfq+YLg4X8QOGEKpCOStVIbg0pK6Z0OgiEYHbUWIXpm+iCQR2StU8Zxw7h0gjnZamMNeOYiqcnLOAc//r5s/JxgwQIzQgtU6ITS2kWG0mLfGmTgtQ7aRgfSGqEsWOmd99wFNNHpWAzywcfXYcLq4Z50hHEUIC3SXnBNlQVPfSsd1VrqHnl0RnBSk7t5ysO0zutSPUwZX9LGj3TkVz8ueCF3m+LrkDHmNRWof+tSbJdmWGOufjiAs4Y1krEiWvNxzdVHhCFhzFjw7tN6zfcfidBiE92tKeGUq53Pr9VvfnktVLyN3LvyVB1jjy3DngHzmhlADRpBCTCgnAKtHIta+cAV+JaHvkRITbAYrto83sfohGWaKrSBKgOKBiE5FcJpw4NWCt5zrJ7GovPRMoUgqcMeqHLcU6ekpCBbb4Tujdmc8R3J1XycFcCAp8u2bh1oAIugORWKt1Q5x6l1raHB2OCCttjG0vMjpgnHn0B9GGKal7nP1dW71Tt0H9epXJxq8/GXYaL/bjO6n+NnTFWZsR+msmDXsZ5rckC/rAkP28Hq3siubFe13+6WMI2z2tXkOfuU1+M1zKVuTGvl+Xw+fwMAAP//AwDUFLHF9gQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1HmTbzozbBb1HDTLGKjLfo24oxiIVppQVGbBob/e0HtBqn71jdhoDM8/OYMX9kMR2Iduz2dfjrTnFdWszzuJSWU5cJyIZ+F74zrtGq08qG14i9Ws/GRvrGOOROjjIG4JkvcoGh5r4fIESlIIVGo1pSe8Hll3SuDsUE6j5GaZf2d0jouM+vYH+OMy9dyNoxNnJYNm7RM9DCvGeZYzPz2cPfIf709PH88XP5aaS3iZkTWMe80AnnDe99bbpRCDq3T3FjoAZUMQg0X2ThnSjNMzYpffpyPS54px041SjeCq8DeaoaQoVjuYaXnb6fiYkd0X+r1Xt4/u1d2psQ6VX+HeWl3E6fxZt36ic40nSCtlG7icjzCjKxmp7ScKOWRdixnSv2yjrkQjdGB91KiB2dah163SjvUgtAL12LAANbYQcW2HbD3wZnBoJIGLRnTO1azHdw0QR6Xubp6v5tGmjOr2cen6jBBHpZ0fIf+6UL04X8ALbZZx7RotXWOjNYagu2VIHQ2Wqv6CMINvSIZSbTBuCCdkDooEXRrnXcIIkRWsw8QX8aZqod71jEhSaH2xAclLTcegUOrA7dW24FkDE5JVrO7Zc7jvC3bWj3MmT6n/b6sYz/DtNLlprcpvoyZYt5SgfCnLWE8pAW3mKsfAZCiEY0Wooi2fNpy9YlwTBQzFRzPabv2+49EWbWL7raUaM7VAfJL9QusL6xjAXyUECAA2BgHagUNAgVY4ZAsWkKj0KEJBq0JIloDvTQIreyHUmE1o5K3ao+4HNpB+0FzFbXgBiJwsBi4UUbqqLXS5j3H6uNUdBC9MISaBxqQmyCBB6M1R92CU3Zwbp/kdyTXsEhRACOdL8u6O7CIntBKroxsuQlBch9ax3vn+9BbT20snh8pzTT9C9SHMaZlXYZcXbNWvUP3aZvLg1Ptufs6zvzvfUb3S/xCqSozhnEu+3Ud61vNjgTrlui4v1fdKzuU5aqe92dLuSZ4G2r2lCHl7XQtS20b13r99vb29g8AAAD//wMA0nzqzvUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9KwLPokxSFCXqZqxTdOEmXiSLtOhtxBnFhGVqQVFOAmP/e0DtBol764UgBnwz72P4ygI8E+vZ7en07oVCWljJkt9KSijDheFCHkXX67avu8p0VraN+JeVzN/Td9azVjsnnSVekyGuUTR8qEfHEclKIVGoRuee8GVh/SsDXyG9eEfVvHymuPg5sJ797QPOX/Ns8JWb5hWrOE+0D0uC4DKZD/vdPf/r9nB8OFxeLbRkcOWR9axrawTqNB+6wXCtFHJo2pprAwOgklao8QLzIVEMMFULPv2aj3MKlFyvKlVXgivLziVDSJApD7DQ8fsps9gsusv1citv1/6VvVBkvSp/mnlpd+Mmf/O8DKuf8OZyJliesspTnE8Uk6fNk03mNEHycyiuk3aTp5BYyR4+FYcJ0jjH5zdGfbro3/8P+cc8/d03cmsi3M1rSKxnsrasZO/BPfpAxf6O9UxIUlh3xEclDdcdAoemttyY2owknW2VZCXbzSH5sM7rUuxDoi9xE8B69gdMC12o30b36BO5tMas6h+Td+EQZ1xdKn75L0UlqlqIDFrTaU3FR0IfySXK+o5xvfb7D0QZtYF2a4wUUnGA9Fj8Ccsj65mFzkmwYAGMcyM1gkaBAoxokQwaQq2wRW01Gm2FMxoGqREaOYy5wkpGOe5i2zDCwSllJBdtbbkGZXmn5MiNpRFNI5ua8I2PxcOUceA6oQlrnt9xbSVwq+uaY91Aq8zYtls0Py25pi9FNhjp5fJXNgYGsSM0kistG66tlbyzTcuHthvsYDpqXOZ8TzHQ9JtR772L8zKPqbguT/HGuo9ryP+92Bbpqw/825bR3eyeKBY5Y/Ahr/c11vP5fP4BAAD//wMAgHCTeTYEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUXU/sNhD9K5Ffuw7+imPnjS5cdUWB1d1VW/VtYk/AJTgrx4F7hfjvVbJw+biq1JcoGttzzpw5M08kwj2ShpweDucPGPNIViSHJSSY0JRpyviemUbVjTSltoLVdfU3WZFwgd9JQ2rlHHcWqUSNVHlW0VZ2jnqPljPumajUnBNuRtI8EQilx4fgsBzGPzCNYYikIX+G6IfHGRtC6fph8mUaetzEMUN0M5mrzfqC/n663V9vj7dGHOfHZfCkIaaWHtAo2ppWUyWEp1DVkioNLXjBLRPd8VmIGVOEvhz93Ru+H3LE7BpRClkyKix5XhEPGWbKLYy4/36YWSwSnc3x1RJefpsn8oCJNGL1KuYx3Ynrw8n92E6h9yfLl6zIIQ0HTDngosb1rngjwVnJSqEFY2RFLsHdhojF5qy47ucSwRmm0EtqsfNUWQ7UKimplxXUQnd1vZR4gSli/y7rZXBpGIcuFy8iFx9wdqfr8whtjzPGF+hHJCuyGXcZood+iHj+Dd2Uj7n2aZqP10PMIU7DNBabmPEmwcv56/v1lBLGXGwh3xa/wXhLGmLBOA4WLIB2rsOKYcc8A81qj9pr9Er42iurvFaWOa2g5cpDxdtujpAV+XXWcDc5h+P4xmYxSd8vHIqXPq37gDF/kJE0hHEUXhqkneCaKuOBQiUt1VrqDrmzteCvMHtIN5hJQzrTti1zrhXKcgfCgvQdk153Voi6BtXJqlZC64rV4HxdC668rNGB7CQzgqzINg1+cvlzq+XSgG0a/kGXZ6lIQzSrKuOgbYVW3IA1qLhWRlusreqAKela4JWBtuUMKuasEQ64tqBaX3U/VDqPNyHiWRgPPXx/h2tKXcpfHIDhHahP178kuMfHId1dHW1cXp3vyYp8neK8EYrNbJHHEOk3PU/05dTnsL9NCB795eDxZx/hPC7FMqHec88MdlR73lFVqZqCc5xK5ZWumKtaNrM5G9wdpmJ2GIQ4T9WPZNe74jS525DR5SnN/P5aeFzvim0PuRvS/YdN8q6wzwKUQnI2d8bjw3EbLRy19wa95lQoXlFlLafGVjVta9PaVhus3MzxesqHKRdf0YeELi8Vv3hxwVzforv7WYxXF7xYlLPZa7vjFluk/V9L7HlF7hHGKeH9sq2bJ7KJEdOCfDYdR3ETL0PfhxHdEP1IGi2lKYU19oXhf95TrCo1Y/L5+fn5XwAAAP//AwB4OEzzGwYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUwW7cIBCGX8Was3EAY2x8izaNukrbrNpVW/WGYZygOHgFOG0U7btXeLdKc6p6QWjMP/zzzeAX8PoRoYfLw+HdE/oUoYTk1hCnXBIqCWV72vWi7euukp1kinc/oAR3g8/QQyuMYUYhqVEiEZY2ZKhHQ6xFxSizlDci59R3EfoX0K6y+OQMVnP8iiG62UMP35y38898t3aVmebFVmGecOtj0t5kM5+2mxvy4XK3v92dTkWMWVw5Cz10bW01doIM3SCJ4NwS3bQ1EVIP2nKmKB9PMucTBq+nKtqH1/vtnDwm0/OK1xUlXMGxBKuTzpYHHXH/fMguVkRXOV6u4XXbv8ATBuh5+QfmKd2FmdzFYxwWN9mL05p0fIjrOmqT5vAMJRzCfMCQHK58LmPEx2F63uv4cH06k7fx3S80S0K7mRefoAdWSyhhBTRNOrnZF2ePm8mhT+vHFJyPzvwrVw0l3H4pdpNO4xwe3/Tjywnz9j8of9Tm3nkstlfQA2XIbd0hGTmTRHRWE93UikhZyxGZUS1nUMJm9sn5ZV5isfUJ78JaEvRwraeIJ3+Xwdy7hCYtIdf5Xea52oXZLiYVr71ktKJVTWkWLemwpOIzWhfQJMxF7MNyzvf1rYRLvoo2SwjoU7HT6b54r+M99KB0Z5hWWmktjRmxoThSS7WkrUVpJVrBbWuFElYKRY0UemDC6oYNY45ACZhHp1inVXE6WCUGIoUQRDSjJB1THWGiHhEZVYxmxSvH4nbKOm06KtDWROFoiVBMEyXqmti60S2XY9uu/P8gOc8DoxmwxafTu1sdSGs7tJIRLlhDhFKMdKppydB2gxpkh43JDm4weJz+AvXRmTDHeUzFeUKKN+g+Lz7/O4p1Wn46T36tPbqazQOGIvdYO5+fyrmtx+Px+BsAAP//AwBa5USZggQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIFntrsVsZIjI1kCgngTH/vqBmAie55UYUWd3VVc0XFuGJWMduT6e/nimmhZUshQ1SQlkuLBfyUbSdaTrdVtZppZ3+j5Us3NN31rHGeC+9I67JEjcoat7rwXNEclJIFKo2uSZ8Xlj3wiBUSM/BUzUtH2lewhRZxz6FiNPX3BtC5cdpxWqeRtrHJUH0Wcw/+909f3t7eHw4XF4ttGRyFZB1rG00ArWG921vuVEKOdSN5sZCD6ikE2q40EJMNEcYqwW/vPbHKUVKvlOV0pXgyrFzyRASZMk9LPT4/ZRVbBbdZbzc4O3YvbBnmlmnyh9mXsrd+DHc+OnpCSLeDCGG5chKdpqnE80p0GbHNuE4QgpTLK5NdmOgmFjJHj4UhxHSMM1Pv3j04TL6/g8mfwf+GCIV+zvWMSFJoW6JD0pabloEDrV23FptB5LeNUqyku2mmEJcp3Up9jHR53lTyTr2N4wLXfTdzv4YEvm0zln6vzZnTd9C2k2YAcFKdpgnXH0qXu2WohKVFvnyYU2nNRXvCcNMPlGe6XFer+V/oyirNtJunWeKqThAOhZvYDmyjjlovQQHDsB6P1AtaBAowIoGyaIlNAobNM6gNU54a6CXBqGW/ZCRrDunW2wLVdey9uiBtxYUN60i3gogrrw33gDUtc478Gpr8TBmHvhWGELNHQ3IjZPAndGao66hUXZomi2OH5ZcE5ci+430fPkamwKL2BJayZWRNTfOSd66uuF90/auty3VPmu+pznS+JNR74Kfp2UaUnFdmOIX696vMX/vYlueryHyb1tkd5P/QnORI4cQ8zZfUz6fz+f/AQAA//8DALuBJ1glBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS0NSN8NuUddNvYgXadEbRY5sIVpqQVGbBsb+94DaDRL31psw0DszfOadeSPRHZB05PZ4/OWEMS+kJnncQoIJoAwo43tmOmU6ZhojmTEA/5CajI/4lXREK++5t0glAlIVWEt7OXgaAlrOeGCiVSWne1lI90bc2AQ8jR6befmEaRnnSDry1xjD/KXUdmPjp3kNTZonfIhLdtGXZv58uHukf9zu9k+7y18LLkXcjIF0BCR6BIdUgjVU9RZpb3CgCMogWCXAm4tsjBlTdFOzhM8/6oc5R8y+E42QDaPCknNNgsuutNy7Bfdfj6WLDdF9iddbePvs3sgJE+lE/R3mJd2Nn8abPB8nPOF0dGnBdOPnw8HFQGpyTPMRUx5xw7K9dJpcHudYXYvdTSPGTGry9FztJpeHOR3esXq+IHj4HwROmHrSEclaCVqjklI6C71gGDR4ANF7x/TQC+QeWWuVtlwzLq1gVragjQ6OWU9q8jLNvZt+XzZ+VrDeBKYFCFRohQKwnqE0OLQaWXAAPRhvgzRaKBOMdNY5bnvU3oIvBvng/OsYsXq4Jx1hHEWQBukgOFBlgqOulZYCSBiQe6sFJzW5m2Me4zqvS/UQM76kjR/pyK9uWvBC7jb51zGjz2sqUP+GUmyX5rD6XP1wAGcNayRjRbTm45qrjxjGhD5jwbtP6zXffyQCxCa6W1PCmKudy6/Vb255LVSc8dzZ8lTwfsCW4cACc8B0QAiAQYmgg7IqgLLMg3I9V8G1vB9KhNQEi+GqzeMMhUERDDUDDFQZI2jfD5wysI4NA/Ss9e84Vk9T0TlvmMIgqcUhUGW5o1ZJSYNsnRYwaC2Gn5BczcdZARzwdNnWy5aFYDAAp0LxliprOTW21bTXprc9GGx96fkRU8TpJ1AfRp/mZR5ydfVu9Q7dxzWWi1NtPv4yRvrvNqP72X/GVJUZuzGWBbuO9VyTA7plTXjYDlb3RnZlu6r9dreEbUQrZE2es0t5PV7D0jRatOJ8Pp+/AQAA//8DAPPar0r1BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1Hm/aI3w27RhZt6ES/aoG8UZxQL0UoLito0MPa/F9RukbhveRMGOsPDb87wjUzhiKQl96fTL2ec8kJqkoetJJgwlBnK+IG5VrmWucYZq7X0f5OaDE/4jbTEqhh59EglGqQKmKad7CMFQM8ZBya0Kj3D54W0byQMDeB5iNjMy5+YlmGeSEv+GiaYv5azw9DEcV6hSfOIu2nJYYrFzB+7hyf6+/3+8Ly//rXgUsTNAKQlRmJEE5BK4x1VnUfaOewpGuXQeCVMdFfZMGVMUxibBb58Px/mPGGOrWiEbBgVnlxqAiGHYrkLCx6+nYqLDdFjqddbefts38gZE2lF/R/Ma7u7OA53y9qNeMbxFNKC6S7Ox2OYgNTklOYTpjzghuWMqZuXIReiMdrgHOfgglXagpNaSAuSIThmNXjwwSjTi6h1D53zVvUKBFdgUKnOkpps4MYx5GGeqpv3h3HAKZOaPL9U+zHkfk7Hd+hfrkR3PwG02CYtkUxLYy0qKWXwphMMwZpojOhiYLbvBPKITHtlPbeMSy+Yl9pYZyEwH0lNPoT4OkxY7R5JSxhHAdIh7QU3VDkINGjpqTHS9Mijt4KTmjzMUx6mdV6Xajdl/Jy2+5KW/BrGBa83vU/xdcgY85oKhE+mhHGfZlhjrr4HgLOGNZKxIlrzac3VR4QhYcxYcBzSeuv3P4kwYhM9rCnhlKt9yK/Vb2F5JS3xwUUefPAhmBh71Ax7BiwYZgENGAQlwILyCozyLBoVOq4gaN71pUJqgiVv1RZx5rW3ogs09B1QZaKlwfaG9r3XiMxyY/Q7jtXzWHQhOqYQJPXYA1WeB+qVlBSkDlaY3lrR/4DkFhbOCmDA83VZr0sG4BAMp0JxTZX3nDqvLe2s63xnHOpYPD9hmnD8AdSHIaZ5mftc3bJWvUP3cZ3Kg1Ntufs6TPSfbUaPc/yCqSozDsNU9us21ktNjhiWNeFxe6/aN7Ivy1UdtmdL+EZoIWvykkPK6+lWlq6xQovL5XL5FwAA//8DALepIcv0BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/jKBSG/4rFtXEBY2x81013tFGn02ianV3tHYbjFNWBCHBnqyr/fYSTqumM9mLvrGPe8/GcF16RU3tAPbo+HH5/BpciKlGyS4gRJjARmNAt6Xre9aSrOilY27b/oBLZW3hBPWq51lRLwDUIwNyQBg/1qLExICmhhrCG55xqF1H/ipStDDxbDZWP3yBE6x3q0V/WGf8911a20pOfTRX8BGsXk3I6N/NlvbrFn6832/vN6VSEmMWVNahHogYNQgGuhewwHyTgoYMRg+AdCMmZ0N1JZl2C4NRURfP0Xt/45CDpnlWsrghmEh1LZFRSueVBRdi+HHIXC6KbHC+X8PLZv6JnCKhn5RvMU7orPdmrfRxmO5mrye921u1W3o12NweVcuESHYI/QEgWFji/WafCy2e/2+V86JOaIqASbSHsrVPT6ccNjGqe0oOfw0Immqf/OoN6pObkUYkWktO0lC3Ow6wmCy79or0ofP9QbCaVRh/2H5b0cGK//h/o75R+tA6K9Q3qEaHATN0BHhkVmHdGYdXUEgtRixGoli2jqEQr75J1s59jsXYJdmdqH/q7DvrRJtBpDnmmv0U22yZ4M+tUvC+YkopUNSFZNKfDnIqvYGwAnSAPsQ3zOd9PEibYIvpkJ/gFz2oOAVwqNio9Fn+o+Ih6JFWnqZJKKiW0HqEhMBJDlCCtAWEEGM5Ma7jkRnBJtOBqoNyohg5jjqASQTZZsfi6rmvTMtZhI6TE3DSAVSM5HmsKvKEwDC18gFvcT1mndEc4mBpLGA3mkioseV1jUzeqZWJsWzZecDobgpJM/dKEf0YwZy99OVn7bXYDz6d7fLp/xnRgBMWM0wZzKSnuZNPioe0GOYgOGp0nW3kX/RvIbxAGH23Kj8iddXavpp+PvG/mFoKD6WI7d1YHH/2YirMtiw/7+jq7/IoVi0W/W4f/XYxx4/UThCIbS1l3sczj8Xj8AQAA//8DAM61a8IMBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJRFCUK2IPhtFgjTWMk7u5ib3wMYyKyaFCU06DIf19Qlmun6GFvwojz+B4zP1Av94BatDwcPh2hjwNaoOimECWUY8IxKbakaVnTEpHTpqzrsv4XLZC7gzfUopppXWgBuAQOmBlSYVVajY0BUZDCEFqxVFM+D6j9gaTLDRydhtwPf0EYnO9Ri/52vfGvqbd0ue78aPLgO1j3Q5S9TsN8Xa/u8JflZvuwOb0aYEjJuTOoRbwEDVwCLrloMFMCsGrAYuCsAS4Y5bo5pbk+Quhllw/m5dLf+NhD1C3NaZkTTAV6XyAjo0wjKznA9u2Qppgouk3xxRSePtsf6AgBtXRxJvNU7kZ37mY/qNF15ibK8AzRBrmHVx9e4Cg7tECH4A8QooOJm4en7DJSQXKSU04JQQt0L/XO9ZCtb7OHLgGWuiEMTIkFWIOZKCQWrCyxKStZU27rmlq0QHcQeuiuqt47HfzgbcxmyrMPfVa+j64f/Thk6z7Cc5DxlPdZdgOgBdqMqnPD7hGkedv6xzH968cuYVmNIUAfs42Mu+xPOexQi4RsdCGFFFJyrS1UBCwxRHJSG+CGg2HU1IYJZjgTRHMmVcGMrAplUwQt0MMYD2M88//9aohtcPs9mMsE24njTSej9WG/NtBHZ12S5vxiMlTXTaiyueaqc9DHDySjFpECqCkbwJYWHLPGSCyrUmDOS26h0KKmxYkw657HnzwRaw0rbdU0XAlVEKC2bhiprSJG21ILUymoaU3qBhqhCVEVKxolNDFQVMwkdMGbUcdfrVBOAj2OfdrObJ1gv7oef+fswsgm+Oi175JgroOntyHC/neqrfz+4AcXE347S/ttgHvfp7/z/s+czclLHy8xSHuQTasHypRKCoJtrRRmlCksjCkwK5iltZIGgKIFuvX6BUKWDCZdP2ly9tTDU7YMeuci6DiG1PmfCdRJzs/nlbnwkX/9tF35AMvDYTFH/zgmkk7Fzgb4cFe+Den5zg/xCvIM7Qk6ex7syk4GjqdT5aYTY0wDhheYsqLCTIgCN6KqsaobJRRvoNIXu2aPYFwAHad62zCmbjOzv/PlMkRnpY5DWp4vXk92mv05P/l1yCvZfmafml9JN1tprlSQZNmn0+GcHPS/7ub7+/v7fwAAAP//AwDjiSUTKAYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2Ikc2a5laUJSTwPC/F9TauaJAXwRhSM45c+bMPJEID0h6cnY8Xj5izAupSQ5bSDChKdOU8QMzvTI9V41iQnSq+5vUJLzDz6QnnXKOO4tUokaqPGvpIEdHvUfLGfdMtKrkhNuF9E8EQuPxMThs5uUPTEuYI+nJnyH6+WPBhtC4aV59k+YJd3HJEF0h8353/o7+fnZzuL453VpwKY+b4ElPtESHGpBKbQ1Vg0U6GBwpamVQWyW0M6dnIWZMEaZm8fdf8f2cI2bXi0bIhlFhyXNNPGQolAdY8PD5WFhsEl2UeL2Ft9/+iTxiIr2oX8U8pXvjpvDmYRnWMPk325fU5JjmI6YccFPjel99JcFZwxqhBWOkJlfg7kLEandRXU+lRHCGKfSSWhw9VZYDtUpK6mULndBj14mR1OQdpojTN1mvgkvzMo+5ehG5+g5nf3Z+GWGYsGC8hWlBUpPdss8QPUxzxMtP6NZ8ynVIazk+n2MOcZ3XpdrFjLcJXs5f35+vKWHM1Q3ku+o3WO5ITywYx8GCBdDOjdgyHJlnoFnnUXuNXgnfeWWV18oypxUMXHlo+TCWCKnJr0XD/eocLstXNptJpmnjUL306XwKGPN3MpKeMI7CS4N0FFxTZTxQaKWlWks9Ine2E/wV5gDpFjPpyWiGYWDODUJZ7kBYkH5k0uvRCtF1oEbZdkpo3bIOnO86wZWXHTqQo2RGkJrcpNmvLv/Yark14CbN/6DLRapiZNa2xsEwCK24AWtQca2MtthZNQJT0g3AWwPDwBm0zFkjHHBtQQ2+Hb+odBlvQ8SLsBwn+PwNrml0I39xAIaPoH64/jbBA36c0/37k42b95cHUpMPaywbodoVi3wMkX7SZaKv1imHw11C8OivZo8/+wjLuFTbhHpmOyOdogqcoMp3lg6CMQrIJdqRd5IV+17M7h5TVRwGIZap+pLsel+dJXcXMrq8psLvr43H9b66mSCPc3r4bpN8U9iPAjRCclY64/HxtI1OW8R7g15zKhRvqbKWU2Pbjg6dGeygDbauKHa95uOaqw/oQ0KXt4pfvLhhnt+hu/9ZjFcXvFiUs+K1/WmLbdL+ryX2XJMHhGVN+LBt6/6J7GLEtCFfrKdR3MWrME1hQTdHv5C+bblolG35C8P/vqd5ww1Xz8/Pz/8CAAD//wMAYBpYrRsGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRU0W7cKhD9FWuejQMYY+O3aNPqrnLbrNpVW/UNw7hBcfAKcNoo2n+vsLdK83R1XxAaOMOZM2d4Aa8fEXq4Pp3ePaFPEUpIbg1xyiWhklB2pF0vup6JqlZCNIx/hxLcLT5DD60whhmFpEaJRFjakKEeDbEWFaPMUt6InFP/iNC/gHaVxSdnsJrjFwzRzR56+Oq8nX/mt7WrzDQvtgrzhHsfk/Ymk/m4392Sf68Px7vDditizODKWehB1mhQaiS1VB0Rg0IydDgSlKJDqQSXpttgzicMXk9VtA+v79s5eUym5xWvK0q4gnMJViedKQ864vH5lFmsEt3keLmG123/Ak8YoOflHzG3dFdmclePcVjcZK+2Nen4ENd11CbN4RlKOIX5hCE5XPW5jhEfh+n5qOPD++1O3sZ3v9AsCe1uXnyCHlgtoYRVoGnSyc2+uHDcTQ59Wg9TcD4681+5aijh7nNxmHQa5/D4ph+fN5n3/0PlD9rcO4/F/gZ6oAy5rTskI2eSiM5qoptaESlrOSIzquUMStjNPjm/zEss9j7hj7CWBD2811PEjd91MPcuoUlLyHV+k9lXhzDbxaTitZeMVrSqKc2gJZ2WVHxC6wKahLmIY1gu+b68hXDJV9BuCQF9Kg463Rf/6HgPPSjdGaaVVlpLY0ZsKI7UUi1pa1FaiVZw21qhhJVCUSOFHpiwumHDmCNQAmbrFKtbkbNxGIUg3TAaIgZNiaaSEqEGbjVT2o65tFcdi7sp47TpqEBbE4WjJUIxTZSoa2LrRrdcjm3Lx78kufiB0Sywxadt7rZ5sbZDKxnhgjVEKMVIp5qWDG03qEF22JjM+RaDx+kvoT44E+Y4j6m4OKR4I92nxee/o1jd8tN58mvt0c1sHjAUucfa+Twql7aez+fzbwAAAP//AwD8UEJ2ggQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7cNhD9FWHOokxSFCXqZqxTdOEmXiSLtOiN4oxiwjK1oCgngbH/HlC7gZPeeiGIAd/Me28eXyHYZ4Iebk+ndy8U0gIlJL+VJJeacc24OPKuV10vVFWbttFt9y+U4O/pO/TQKueEM8Rq0sQU8oYN9egYIhnBBXLZqNzTflmgfwXrK6QX76ial88UFz8H6OFvH3D+mmdbX7lpXrGK80T7sCQbXCbzYb+7Z3/dHo4Ph8urhZYMrjxCD7omR9oSq7XpmBoMsaGjkZFWHWmjpHbdBeZDohjsVC349DYf5xQouV5Wsq44kwbOJaBNNlMe7ELH76fMYrPoLtfLrbxd+1d4oQi9LH+aeWl34yZ/87wMq5/w5nImuzxllac4nygmT5snm8xpssnPobhO2k2eQoISHj4Vh8mmcY7Pvxn16aJ//z/kH/P0d9/IrYlwN68hQQ+iNlDCe+sefaBifwc9cEES647YKIVmqkPLbFMbpnWtRxLOtFJACbs5JB/WeV2KfUj0JW4CoIc/7LTQhfptdI8+kUtrzKr+0TkLhzjj6lLx5r/gFa9qzjNoTac1FR8JfSSXKOs7xvXa7z8QqeUG2q0xUkjFwabH4k+7PEIPxnZOWGONtdq5kRpOI0duNW+RNGpCJbFFZRRqZbjTyg5CoW3EMOYKlEB53cWWsMFRSzg2jHNjmRpQM4Pk2Fi3sh4bbQbKFN98LB6mjLOu44qwZoZGZMoIy4yqa4Z1Y1upx7aV4y+WXLcveDYY6eXyVy4ZR+wItWBSiYYpYwTrTNOyoe0GM+iOGpc531MMNP1i1Hvv4rzMYyqu4Sl+s+7jGvJ/L7YgffWBfdt2dDe7J4pF3rH1Icf7utbz+Xz+AQAA//8DAJKaUgc2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUUW/cKBDHv4rFs3EAY2z8Fm1a3SrXy6pZ9U73NoahQXHwCnDaKMp3P+HdU5unqi8IjfkPM7/541cS4AnJSK5Ppw/PGHIiNcl+CwkmFGWKMn7kbGRy5G2jVSc1a/8lNfG3+EJG0ktjuNFIW1RIpWUdnVpnqLWoOeOWiU6WnPA1kfGVgG8sPnuDzZK+YEx+CWQkf/tgl2/lbvCNmZfVNnGZcR9ShmBKMX/td7f0z+vD8e5wPpUwFXHjLRmJMtr0xmmqcBioVKynA+84Fb0DAeiA93iW+ZAxBpibZB9/3G+XHDCbUTSibRgVmrzVxEKGUvIECY8vp1LFhuimxOstvG3HV/KMkYyi/h/mOd2Vmf3VU5pWP9ur85ohPaZtdWDyEl9ITU5xOWHMHjc+1ynh0zS/HCE9fjyfKdv04TuaNaPdLWvIZCS8HUhNNkDzDNkvobrUuJs9hrx9zNGH5M2vcrWkJnf31WGG7Jb49G4e92fM+9+g/AnMgw9Y7W/ISBhHYdsBqRNcUTlYoNC1mirVKofc6F5wUpPdErIP67Kmah8yfo1bS2QkH2FOJendfXUdzYPPaPIaS5//qOKrQ1zsanL1Y5acNaxpGSuiNZ/WXH1G6yOajKWJY1wv+b68lwglNtFujRFDrg6QH6o/ID2QkWgYDAcNGkAZ47Bj6JhloFhvUVmFVgrbW6mlVVIzoyRMXFro+ORKhNQEi3Wqza2mYwMDJ+ngJk6lU4qCM5YCIpNa8M4y+45jdTcXHZiBSbQt1egslZoD1bJtqW076IVyfS/cT0gufuCsALb4fH535/di7YBWcSok76jUmtNBdz2d+mHSkxqwM6XmW4wB559AffImLmlxubo4pHqH7vMayr+j2tzyzQf6fZvRzWIeMVZlxuBDeSqXsb69vb39BwAA//8DAJ/SwMyCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/cNhD9KwKvXcr8EkXq5toOunAdG9lFW/Q2Ikc2a5laUJSTwPB/L6i1EztBgV4EYUjOe/PmzTyRCA9IOnJ6OFw8Yswz2ZAc1pBgQlOmKeN7zjqmOi5r23KmLPubbEi4xK+kI61yjjuLVKJGqjxraC8HR71Hyxn3TDSq5ITbmXRPBELt8TE4rKf5D0xzmCLpyJ8h+ulzwYZQu3FafJ2mEbdxzhBdIfNxe3ZJfz+92V/fHG/NOJfHdfCkI9pZ17rBUo3GUKVZSw1vOBXtAAJwAN7i8VmIGVOEsZ79/Xd8P+WI2XWiFrJmVFjyvCEeMhTKPcy4/3ooLFaJzkt8s4bX3+6JPGIindi8inlMd+LGcPIw90sY/cn6JRtySNMBUw64qnG9q76T4KxmtdCCMbIhV+DuQsRqe15dj6VEcIYp9JJaHDxVlgO1SkrqZQOt0EPbioFsyCWmiOObrFfBpWmehly9iFy9w9mdnl1E6EcsGB9gnItO23mXIXoYp4gXX9At+Zhrn5ZyfDbFHOIyLXO1jRlvE7ycv74/W1LCmKsbyHfVbzDfkY5YMI6DBQugnRuwYTgwz0Cz1qP2Gr0SvvXKKq+VZU4r6Lny0PB+KBGyIb8WDXeLczjP39msJhnHlUP10qezMWDM72QkHWEchZcG6SC4psp4oNBIS7WWekDubCv4K8we0i1m0pHB9H3PnOuFstyBsCD9wKTXgxWibUENsmmV0LphLTjftoIrL1t0IAfJjCAbcpMmv7j8Y6vl2oCbNP2DLhepipFZ0xgHfS+04gasQcW1Mtpia9UATEnXA28M9D1n0DBnjXDAtQXV+2b4ptJFvA0Rz8N8GOHrG1xT61r+4gAMH0D9cP1Dggf8PKX7j0cb1x8v9mRDPi2xbIRqWyzyOUT6RZeJvlrGHPZ3CcGjv5o8/uwjLONSrRPayB6RNYaitj1VpmfUKFRUqsZ7Cc5IV9Q4n9w9pqo4DEIsU/Ut2fWuOk3uLmR0eUmF318rj+tddTNCHqb08G6TvCnsRwFqITkrnfH4eNxGxy3ivUGvORWKN1RZy6mxTUv71vS21wYbVxS7XvJhydUn9CGhy2vFL15cMc/u0N3/LMarC14sylnx2u64xVZp/9cSe96QB4R5SfiwbuvuiWxjxLQiny/HUdzGqzCOYUY3RT+TTvPG1K3m+oXhf94TzNbCSvX8/Pz8LwAAAP//AwCR2jkWGwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS/zQzbBbdOumXsSLtOhtRI5sIlppQVGbBsb+94DyBol7600YaGZePvPOvJIJjkg6cns6/XLGKS+kJjluIcGEpkxTxg+cdcx0rG2U1kZo9g+pSXzAr6QjRnnPvUMqUSNVgbW0l4OnIaDjjAcmWlVqwvNCulcCsQl4jh6befmEaYnzRDryV5zC/KX0htj4cV5Dk+YRd9OSYfJFzJ+7uwf6x+3+8Lh/+2vBpSQ3MZCOOFA9Wh2otaaninGk1jtLBXBurETlXBEBsYlTxjTB2Czh84/+Yc4TZt+JRsiGUeHIpSYBMhTJPSx4+HoqKjZE9yVeb+Hts3slZ0ykE/V3mG/lbvwYb/J8GvGM4wnSgunGz8cjTIHU5JTmE6YcccOyvXQcIcd5qq7N7saIUyY1eXyq9iPkYU7Hd6ye3hDs/geBM6aedESyVmpjUEkpweleMAxGe61F74GZoRfIPbLWKeO4YVw6wZxstbEmAHOe1OR5nHsYf182fk6w3gZmhBao0AmltfMMpcWhNcgCaN1r612Q1ghlg5XgALjr0XinfVH2AfxLnLDa3ZOOMI4iSIt0EFxTZQNQaKWjWks9IPfOCE5qcjdPOU7rvC7Vbsr4nDZ+pCO/wrjgG7nb5F9iRp/XVKD+rUuzfZrD6nP1wwGcNayRjJWkNZ/WXH3EEBP6jAXvIa3Xev9JEVpsSXdrSjjlag/5pfoNlpdtJtZzcOWp2vsBW4YDCww0MwF10BiUCCYop4JWjnmtoOcqQMv7oURITbAYrto8zl0QDgZHBQdFlWKaQguGSudkb4zCtmfvOFaPY8kDb5nCIKnDIVDlOFCnpKRBtmCEHowRw09IrubjrAAOeH7b1k2BDsFi0JwKxVuqnOPUutbQ3tje9dpi64vmB0wTjj+B+hB9mpd5yNXVu9U7dB/XqVycavPxlzjRf7cZ3c/+M6aqzBjiVBbsOtZLTY4Iy5rwuB2s7pXsy3ZVh+1uibaRVtbkKUPK6+kabXXDneOXy+XyDQAA//8DAA1lMYT0BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjz+0M3w25Rw02ziI226G1EjmIiWmlBUZsGxv73gvIWiXvrjRhwhi+feWdeyQxHJD25PZ1+OuNcVtKSkvaQYMJQZijjz5z1zPZMd5pp5rX5i7QkPeI30hOrQuDBI5VokKrINB3kGGiM6DnjkQmtak34vJL+lUDqIp5TwG5Zf8e8pmUmPfkjzXH5Wt+G1IVp2WKXlwkf5rXAHKqY3x7uHumvt4fnj4e3WyuuNblLkfTEgxrQmUidswNVjCN1wTsqgHPrJCrvqwhIXZoL5hmmbo1fvr8flzJjCb3ohOwYFZ5cWhKhQJU8wIrP305VxY7ovsbbPbwf+1dyxkx60f4L863cTZjSzboNE55xOkFeMd+E5XiEOZKWnPJywlwS7ljOmIdlTaUSDcGCc5xHB1ZpG53UQtooGUbHrI4+ejDKjCJoPcbBeatGFQVX0aBSgyUt2cFNE5S0zM1V+92UcC6kJR+fmsMEZVzy8R36pzeiD/8DaJVNeiKZlsZaVFJK8GYQDKM1wRgxBGB2HATygEx7ZT23jEsvmJfaWGcjMB9ISz5AeEkzNg/3pCeMo4jSIR0FN1S5CBS09NQYaUbkwVvBSUvulrmkeVu2tXmYC37O+39JT36GacW3n97m8JIKhrLlCuFPU2Uf8hK3UJrvBuCsY51krCZt5bSV5hPGlDEUrDie83at958UYcSedLfljHNpDlBeml9gfdkZusDBgwcwIYyoGY4sMjDMRjTRYFQi2qi8ikZ5FoyCgasImg9jjZCWYPVbs1vcOmYAnKRMiJGq0RoKSo5UKxiUAe/C4N9xbD5ONQ+CYwqjpB7HSJXnQL2SkkapwQozWivGH5BczcJZBRzx/DasuwITo8NoOBWKa6q859R5belg3eAH41CHquAR84zTD6A+pJCXdRlLc/Va8w7dp22uC6fZffc1zfTvvUf3S/iCuak9hjTX+bq29dKSI8K6ZTzu+6p/JYc6XM3zvraE7qSTLXkqkMt2uka16bj3/HK5XP4BAAD//wMAAznKIvMEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUT2/jKBT/KhZn4wLG2ETaQ5TOaKNOp1Gb2V3tDcOjQXVMhHE61ajffYXjTNLRHPZmPcN7v3+PH6hXe0ALtDwcPh2hjwPKUXRTiREmMBGY0C0lC1IvSFVI3nBZiX9RjtwdvKEFqrnWVEvAJQjA3JAKt6XV2BiQlFBDWMVTT/U8oMUPpFxh4Og0FH74C8LgfI8W6G/XG/+aZitX6M6Ppgi+g3U/RNXrBObrenWHvyw324fN6dQAQ7pcOIMWSCreQiMMbpq6xZxQwI2WDWaK0ropgUuZQChXuD5C6FVXDOblMt/42EPUC1awsiCYSfSeI6OiSpBbNcD27ZBQTBLdpno+lafPxQ90hIAWLD+LeWp3ozt3sx/a0XXmJqrwDNEGtYdXH17gqDqUo0PwBwjRwaTNw1N2gURJQQomGCEoR/dK71wP2fo2e+gSYaUbwsGUWII1mEuqsORliU1ZqZoJW9fMohzdQeihu+p673Twg7cxmyXPPsxZ+T66fvTjkK37CM9BxdO9z6obAOVoM7adG3aPoMzb1j+O6V8/donLagwB+phtVNxlf6phN/nSaKqkkkoJrS1UBCwxRAlSGxBGgOHM1IZLbgSXRAuuWsqNqmhrUwXl6GGMhzGe9f9+BWIb3H4P5oJgO2m86VS0PuzXBvrorEvWnE9Mgeq6iVU291x1Dvr4QWS0QIQCM2UD2DIqMG+MwqoqJRaiFBaoljWjJ8Gsex5/6kSsNby0VdOIVraUALN1w0ltW2K0LbU0VQs1q0ndQCM1IW3FadNKTQzQipvELngz6vhrFMrJoMexT9uZrRPtV9fj74JfFNkEH732XTLMdfD0NkTY/861ld8f/OBi4m9na78NcO/79Hfe/1mz+fLSx0sN0h5k0+pRA5WudYUr0C3mlnLctopjyyrGKC9Bihrl6NbrFwhZCphy/eTJOVMPT9ky6J2LoOMY0uR/JlInOz+fV+aiR/H103blAywPh3yu/nFMIp2anQPw4V35NqTjOz/EK8oztSfo7BnYVZwMHE9PlUtFYUwDRlDMOK0wl5LiRlY1buumla1ooNKXuGaPYFwAHad+2zCmabOyv8vlMkRnlY5DWp4vXk9xmvM5H/kV5JVtP2+fhl9ZN0dp7kRJiuzT6eGcEvS/3s339/f3/wAAAP//AwBZ8qiSKAYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUUW/jKBDHv4rFs3EBY2z81ktvdVG322ib2zvdG4ZxiupABLh7VZXvvsJJ1XRX93Bv1piZ+c9v/vCKnNoD6tH14fD7M7gUUYmSXUKMMIGJwIRuKelJ25OmampOJO/+QSWyt/CCetRyramWgGsQgLkhDR7qUWNjQFJCDWENzzXVLqL+FSlbGXi2Giofv0GI1jvUo7+sM/577q1spSc/myr4CdYuJuV0FvNlvbrFn6832/vN6VSEmJMra1CPpOIDdMLgrmsHzAkF3GnZYaYobbsauJRZhLKVdQmCU1MVzdN7f+OTg6R7VrG6IphJdCyRUUllyYOKsH05ZBULopscL5fw8tm/omcIqGflG8xTuSs92at9HGY7mavJ73bW7VbejXY3B5Vy4xIdgj9ASBYWOL9Zp8LLZ7/b5Xrok5oioBJtIeytU9Ppxw2Map7Sg5/DQiaap/86g3qk5uRRiRaS07S0Lc7DrCYLLv2Se9H4/qHYTCqNPuw/LOnhxH79P9DfKf1oHRTrG9QjQoGZugM8Miow74zCqqklFqIWI1AtW0ZRiVbeJetmP8di7RLsztQ+6LsO+tEm0GkOeaa/RW62Cd7MOhXvC6akIlVNSE6a02FOxVcwNoBOkIfYhvlc76cUJtiS9MlO8Aue1RwCuFRsVHos/lDxccHRaaqkkkoJrUdoCIzEECVIa0AYAYYz0xouuRFcEi24Gig3qqHDmCOoRJBNViy+boUGM3YdbuuWYd4OFA/SGMwG2Q3jCHTk8AFucT/lPKU7wsHUWMJoMJdUYcnrGpu6US0TY9uy8YLT2RCUZOqXJvwzgjl76cvJ2m+zG3g+3eNFpzCmAyMoZpw2mEtJcSebFg9tN8hBdNDoPNnKu+jfQH6DMPhoU35E7qyzezX9fOR9M7cQHEwX27mzOvjox1ScbVl82NfX2eVXrFgs+t06/O9ijBuvnyAU2VjKuotlHo/H4w8AAAD//wMAFYxWiAwFAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwW7cOAyGX8XQ2XIkWZYt34LJLjpIuxm0QVv0Rot0R6gjD2Q5bRHMuy/kmSLt3vYm/BLJnx+pFxbgiVjPbk+nv54ppIWVLPlNUkIZLgwX8lGKXrS9lJVqbNe28gsrmb+nn6xnrXZOOku8JkNco2j4UI+OI5KVQqJQjc454evC+hcGvkJ69o6qeflIcfFzYD375APO33Nt8JWb5hWrOE+0D0uC4LKZf/a7e/729vD4cLi8WmjJwZVH1jMLeqDOIO+6duBaSOKdsx1XIGXb1aStzSbAVz4kigGmasFvr/VxToGS61Wl6kpwZdm5ZAgJsuUBFnr8ecouNkR3WS83eTv2L+yZIutV+QvmJd2Nm/yNm5+eIODN6INfjqxkpzifKCZPG46tw2mC5OdQXIvsJk8hsZI9fCgOE6Rxjk9/MPpwaX3/Pzp/B+7oAxX7O9YzIUlh3REflTRcdwgcmtpyY2ozknS2VZKVbDeH5MM6r0uxD4m+xs0l69nfMC108Xcb3dEncmmN2fpnk4vRD592M2ZBsJId4oyrS8UrbikqUdUiXz6s6bSm4j2hj+QS5Z4e43pN/58QZdQWtFtjpJCKA6Rj8QaW4waicxIsWADj3EiNoFGgACNaJIOGUCtsUVuNRlvhjIZBaoRGDmNWsu883WJbKNM4I6lDPgxkuB4Gy622AwdrEGrV0NjkiFesxcOU48B1QhPW3NKIXFsJ3Oq65lg30Coztq0af0NynbgUmTfS8+VrXBwgdoRGcqVlw7W1kne2afnQdoMdTEeNyw7uKQaafgP1zrs4L/OYiuvCFH+ge7+G/L2LbXm++8B/bCO7m903ikUeOfiQt/k65fP5fP4XAAD//wMAyKqZXSUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXexIveXNtBF6ljI7toi76NyJHNWqYWEuUkMPzvBbV2rijQF0EYknPOnDkzTyTBA5KOnB2Pl4+Y8kJqkuMWEkxoyjRl/MBZx0zHeSM0Y9KJv0lN4jv8TDpilPfcO6QSNVIVWEt7OXgaAjrOeGCiVSUn3C6keyIQm4CP0WMzLX/gvMQpkY78GVOYPhZsiI0fpzU08zTiLi0Zki9k3u/O39Hfz24O1zenWwsu5XETA+mIA9Wj1YFaa3qqGEdqvbNUAOfGSlTOFRIQm5gyzgnGZgn3X/HDlBNm34lGyIZR4chzTQJkKJR7WPDw+VhYbBJdlHi9hbff7ok84kw6Ub+KeUr3xo/xzcPSr3EMb7Yvqclxno4454ibGtf76isJzhrWCC0YIzW5An8XE1a7i+p6LCWCt0xhkNThEKhyHKhTUtIgWzBCD8aIgdTkHc4Jx2+yXkU/T8s05OpF5Oo7nP3Z+WWCfsSC8RbGBUlNdss+QwowTgkvP6Ff8ynXYV7L8fmUckzrtC7VLmW8neHl/PX9+TrPmHJ1A/mu+g2Wu61F1nNw4AC09wO2DAcWGGhmAuqgMSgRTFBOBa0c81pBz1WAlvdDiZCa/Fo03K/e47J8ZbOZZBw3DtVLn87HiCl/JyPpCOMogrRIB8E1VTYAhVY6qrXUA3LvjOCvMAeYbzGTjgy273vmfS+U4x6EAxkGJoMenBDGgBpka5TQumUGfDBGcBWkQQ9ykMwKUpObeQqrzz+2Wm4NuJmnf9DnIhXpiGZtaz30vdCKW3AWFdfKaofGqQGYkr4H3lroe86gZd5Z4YHrMgChHb6odJluY8KLuBxH+PwNrm10I3/xAJYPoH64/naGB/w4zffvTzZu3l8eSE0+rKlshGpXLPIxJvpJl2G6WsccD3czQsBwNQX82UdYxqXaJhSCYb0znEoYLFVMCQq+FdT3ivUemfZYXlxM/h7nqjgMYipT9SXZ9b46m/1dzOjzOhd+f208rvfVzQh5mOaH7zbJN4X9KEAjJGelMwEfT9to46hDsBg0p0LxlirnOLWuNbQ3tne9ttj6otj1mo9rrj5giDP6vFX84sUN8/wO/f3PYry64MWinBWv7U9bbJP2fy2x55o8ICzrjA/btu6eyC4lnDfki/U0irt0FccxLuinFBbStUa2jRNSvjD873vWNM4I8/z8/PwvAAAA//8DAK8xVv8bBgAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTQW/cNhCF/4rA81ImKYoidTPWKbpwUy+SRVv0NuKMYsIytaAoJ4Hh/15Qu4WT3noRhAEf571vhq8swjOxnt2ezx9eKOaF7VgOW0kJZbgwXMiTFL3oeilr1XbCafs327FwT99ZzzrtvfSOeEOGuEbR8qEZPUckJ4VEoVpd7oQvC+tfGYQa6SV4quflD0pLmCPr2Z8h4vy19IZQ+2lesU7zRIe4ZIi+mPn9sL/nv90eTw/Hy6mFliKuA7KeOdADWYPc2m7gWkji1jvLFUjZ2Ya0c8UEhDrETCnCVC/49N4f5xwp+17VqqkFV4697RhChmJ5gIVO38/FxYbortR3W3n77V/ZCyXWq92/MC/X3fgp3DwvwxomvLl8MyxPJeU5zWdKOdDGZIs5TZDDHKtrp/0UKGa2Yw+fq+MEeZzT80+gPl/yH/5H/FPp/uEb+TUT7uc1ZtYzqSXbsY/gH0Ok6nDHeiYkKWws8VFJw7VF4NA2jhvTmJGkd50qmv0cc4jrvC7VIWb6krYArGe/wLTQxfpt8o8hk89rKqn+MsXHMc24+ly985eiFnUjRBGt+bzm6hNhSOQzlXyntF7v+49EGbWJ9mtKFHN1hPxY/QrL4wbFegkOHIDxfqRW0ChQgBEdkkFDqBV2qJ1Go53wRsMgNUIrh7FU2I5RGXe1bVgroRkctFxB03INFrj1jeaGvLUo2w6oWHznWD1MRQfeCk3YcEcjcu0kcKebhmPTQqfM2HVq/AHJdfpSFMBIL5e3sjkwiJbQSK60bLl2TnLr2o4PnR3cYCy1vni+pxRp+gHUx+DTvMxjrq7LU/2E7tMay3uvtkX6GiL/ts3obvZPlKoyYwixrPd1rG9vb2//AAAA//8DAMn6CWw2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS5Som7FO0IWbepEs0qI3ijOKCcvUgqScGMb+94LaLRKfil4IYsiZeeeZId9IsM9IBnJ7On14wZATqUn2m0kwoSnTlPEjZwPrBs4b0apWcvk3qYm/x1cykE45x51BKlEjVcBaOsrJUQA0nHFgolUlpv2WyPBGrG8AX7zDZklfMSa/BDKQP32A5XvJbX3j5mWFJi4z7kPKNrgi5o/97p7+fns4PhwutxKm4tx4IAMxVo3Ya6B9341UMY60d6anwnLe9RKVMUWE9Y0PGWOwc5Pg6Wd+WHLA7AbRCNkwKgw51wRstkXyaBMeX09FxYbortjrzbxthzfygpEMov4X5iXcjZv9zXMaVz/DzWXNNj2lbZ2sy0t8JTU5xeWEMXvc+NymhM/j/Hq06enj5U7Zpg8/0K0ZYbesIZOBcNmTmmyA5tlmv4TqqnE3ewx5O8zRh+Tdf8WSpCYPX6rDbPO0xOd3/fhywbz/H5Q/WffoA1b7OzIQxlGA7JFOgmuqerDUttJQraWekDvTCU5qsltC9mFd1lTtQ8ZvcSuJDOSjnRNe9N1G9+gzurzGUudfuiQ7xAVWl6ufveSsYY1krDit+bTm6jOCj+gyliKOcb3G+/reRWixOe3WGDHk6mDzY/WbTY9b5b3j1lhjrXZuwpbhxIBZzTpADRpBCehAGQVaGea0siNXYFs+TsVCaoJldKptWrlxndbA6Qi8pUpaoNYqRp2TTpgOnWvhHcfqYS5+1vVMIUhqcAKqDLfUKCkpyNZ2Qk9dJ6ZfkFzngbMCGPDl8u42BRqgR9CcClUUGMNpb9qOjl0/mlH32Lqi+R5jwPkXUJ+8i0taplxdJ6R6h+7zGsrfUW3T8t0H+mPr0d3injBWpcfWh/JUrm09n8/nfwAAAP//AwCG30+3ggQAAA==

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,6 +59,18 @@ public sealed class LocationsController(ILocationService locations) : Controller
return RedirectToAction(nameof(Details), new { id = model.FromLocationID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropRelationship(LocationRelationshipEditViewModel model)
{
if (model.FromLocationID != model.ToLocationID)
{
await locations.SaveRelationshipAsync(model);
}
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ArchiveRelationship(int id, int returnLocationId)

View File

@ -59,6 +59,18 @@ public sealed class StoryAssetsController(IAssetService assets) : Controller
return RedirectToAction(nameof(Details), new { id = returnAssetId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropDependency(AssetDependencyEditViewModel model)
{
if (model.SourceAssetID != model.TargetAssetID)
{
await assets.SaveDependencyAsync(model);
}
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteDependency(int id, int returnAssetId)

View File

@ -47,4 +47,61 @@ public sealed class TimelineController(ITimelineService timeline) : Controller
await timeline.ArchivePresetAsync(id);
return RedirectToAction(nameof(Index), new { projectId, bookId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropMoveScene(TimelineSceneDropMoveViewModel model)
{
var summary = await timeline.MoveSceneByDropAsync(model);
if (summary is not null)
{
TempData["TimelineDragMessage"] = $"Scene moved. Validation found {summary.WarningCount} warning{(summary.WarningCount == 1 ? "" : "s")}.";
}
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.BookID, selectedSceneId = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropCharacter(TimelineSceneCharacterDropViewModel model)
{
await RunDropActionAsync(() => timeline.AddCharacterByDropAsync(model), "Character added to scene.");
return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID, selectedSceneId = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropAssetEvent(TimelineAssetEventDropViewModel model)
{
await RunDropActionAsync(() => timeline.AddAssetEventByDropAsync(model), "Asset event added to scene.");
return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID, selectedSceneId = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropAssetDependency(TimelineAssetDependencyDropViewModel model)
{
await RunDropActionAsync(() => timeline.AddAssetDependencyByDropAsync(model), "Asset dependency saved.");
return RedirectToAction(nameof(Index), new { projectId = model.ReturnProjectID, bookId = model.ReturnBookID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DropLocationRelationship(TimelineLocationRelationshipDropViewModel model)
{
await RunDropActionAsync(() => timeline.AddLocationRelationshipByDropAsync(model), "Location relationship saved.");
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.ReturnBookID });
}
private async Task RunDropActionAsync(Func<Task> action, string successMessage)
{
try
{
await action();
TempData["TimelineDragMessage"] = successMessage;
}
catch (InvalidOperationException ex)
{
TempData["TimelineDragMessage"] = ex.Message;
}
}
}

View File

@ -40,6 +40,7 @@ public interface ISceneRepository
Task<IReadOnlyList<ChapterOption>> ListChapterOptionsAsync(int projectId);
Task<SceneMovePreview?> GetMovePreviewAsync(int sceneId, int targetChapterId, string position);
Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber);
Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position);
Task RenumberChapterAsync(int chapterId);
}
@ -752,6 +753,15 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
commandType: CommandType.StoredProcedure);
}
public async Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Scene_MoveRelative",
new { SceneID = sceneId, AnchorSceneID = anchorSceneId, Position = position },
commandType: CommandType.StoredProcedure);
}
public async Task RenumberChapterAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();

View File

@ -39,6 +39,7 @@ public interface ISceneService
Task<int> SaveAsync(SceneEditViewModel model);
Task ArchiveAsync(int sceneId);
Task MoveAsync(int sceneId, string direction);
Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position);
Task<SceneMovePreviewViewModel?> GetMovePreviewAsync(SceneMoveRequestViewModel request);
Task<ContinuityValidationSummary?> MoveToChapterAsync(SceneMoveRequestViewModel request);
Task RenumberChapterAsync(int chapterId);
@ -132,6 +133,11 @@ public interface ITimelineService
Task<int> SavePresetAsync(TimelinePresetSaveViewModel model);
Task<TimelineViewPreset?> GetPresetAsync(int timelineViewPresetId);
Task ArchivePresetAsync(int timelineViewPresetId);
Task<ContinuityValidationSummary?> MoveSceneByDropAsync(TimelineSceneDropMoveViewModel model);
Task AddCharacterByDropAsync(TimelineSceneCharacterDropViewModel model);
Task AddAssetEventByDropAsync(TimelineAssetEventDropViewModel model);
Task AddAssetDependencyByDropAsync(TimelineAssetDependencyDropViewModel model);
Task AddLocationRelationshipByDropAsync(TimelineLocationRelationshipDropViewModel model);
}
public interface IAnalyticsService
@ -487,6 +493,9 @@ public sealed class SceneService(
}
}
public Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position) =>
scenes.MoveRelativeAsync(sceneId, anchorSceneId, position);
private async Task<(Project Project, Book Book, Chapter Chapter)?> GetContextAsync(int chapterId)
{
var chapter = await chapters.GetAsync(chapterId);
@ -770,6 +779,9 @@ public sealed class TimelineService(
var allBooks = await books.ListByProjectAsync(projectId);
var lookupData = await lookups.GetAllAsync();
var assetLookupData = await assets.GetLookupsAsync();
var characterLookupData = await characters.GetLookupsAsync();
var locationLookupData = await locations.GetLookupsAsync();
var plotLines = await plots.ListPlotLinesAsync(projectId);
var plotThreads = await plots.ListPlotThreadsByProjectAsync(projectId);
var allAssets = await assets.ListAssetsAsync(projectId);
@ -830,9 +842,15 @@ public sealed class TimelineService(
PlotLineOptions = plotLines.Select(x => new SelectListItem(x.PlotLineName, x.PlotLineID.ToString(), x.PlotLineID == filter.PlotLineID)).ToList(),
PlotThreadOptions = plotThreads.Select(x => new SelectListItem(x.ThreadTitle, x.PlotThreadID.ToString(), x.PlotThreadID == filter.PlotThreadID)).ToList(),
AssetOptions = allAssets.Select(x => new SelectListItem(x.AssetName, x.StoryAssetID.ToString(), x.StoryAssetID == filter.StoryAssetID)).ToList(),
AssetEventTypeOptions = ChapterService.ToSelectList(assetLookupData.AssetEventTypes, x => x.AssetEventTypeID, x => x.TypeName),
AssetStateOptions = assetLookupData.AssetStates.Select(x => new SelectListItem(x.StateName, x.AssetStateID.ToString())).ToList(),
AssetDependencyTypeOptions = ChapterService.ToSelectList(assetLookupData.AssetDependencyTypes, x => x.AssetDependencyTypeID, x => x.TypeName),
CharacterOptions = allCharacters.Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString(), x.CharacterID == filter.CharacterID)).ToList(),
CharacterRoleOptions = characterLookupData.RoleTypes.Select(x => new SelectListItem(x.TypeName, x.CharacterRoleInSceneTypeID.ToString())).ToList(),
PresenceTypeOptions = characterLookupData.PresenceTypes.Select(x => new SelectListItem(x.TypeName, x.PresenceTypeID.ToString())).ToList(),
PovCharacterOptions = allCharacters.Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString(), x.CharacterID == filter.POVCharacterID)).ToList(),
LocationOptions = allLocations.Select(x => new SelectListItem(x.LocationPath, x.LocationID.ToString(), x.LocationID == filter.LocationID)).ToList(),
LocationRelationshipTypeOptions = ChapterService.ToSelectList(locationLookupData.RelationshipTypes, x => x.LocationRelationshipTypeID, x => x.TypeName),
WarningSeverityOptions = warningLookups.Severities.Select(x => new SelectListItem(x.SeverityName, x.WarningSeverityID.ToString(), x.WarningSeverityID == filter.WarningSeverityID)).ToList(),
WarningTypeOptions = warningLookups.WarningTypes.Select(x => new SelectListItem(x.TypeName, x.WarningTypeID.ToString(), x.WarningTypeID == filter.WarningTypeID)).ToList(),
RevisionStatusOptions = lookupData.RevisionStatuses.Select(x => new SelectListItem(x.StatusName, x.RevisionStatusID.ToString(), x.RevisionStatusID == filter.RevisionStatusID)).ToList(),
@ -865,6 +883,134 @@ public sealed class TimelineService(
public Task ArchivePresetAsync(int timelineViewPresetId) => presets.ArchiveAsync(timelineViewPresetId);
public async Task<ContinuityValidationSummary?> MoveSceneByDropAsync(TimelineSceneDropMoveViewModel model)
{
if (model.SceneID == model.AnchorSceneID)
{
return null;
}
await scenes.MoveRelativeAsync(model.SceneID, model.AnchorSceneID, model.Position == "Before" ? "Before" : "After");
await warnings.FinaliseSceneDependencyWarningsAsync(model.ProjectID, model.BookID);
return model.BookID.HasValue
? await warnings.ValidateBookAsync(model.BookID.Value)
: await warnings.ValidateProjectAsync(model.ProjectID);
}
public async Task AddCharacterByDropAsync(TimelineSceneCharacterDropViewModel model)
{
var characterId = model.CharacterID.GetValueOrDefault();
if (characterId == 0)
{
throw new InvalidOperationException("Choose a character to drop onto the scene.");
}
await characters.SaveSceneCharacterAsync(new SceneCharacter
{
SceneID = model.SceneID,
CharacterID = characterId,
RoleInSceneTypeID = model.RoleInSceneTypeID,
PresenceTypeID = model.PresenceTypeID,
LocationID = model.LocationID
});
}
public async Task AddAssetEventByDropAsync(TimelineAssetEventDropViewModel model)
{
var assetId = model.StoryAssetID.GetValueOrDefault();
if (assetId == 0)
{
throw new InvalidOperationException("Choose an asset to drop onto the scene.");
}
var lookupData = await assets.GetLookupsAsync();
var eventType = lookupData.AssetEventTypes.FirstOrDefault(x => x.AssetEventTypeID == model.AssetEventTypeID)
?? lookupData.AssetEventTypes.First();
var title = string.IsNullOrWhiteSpace(model.EventTitle) ? eventType.TypeName : model.EventTitle.Trim();
var description = model.EventDescription;
if (!string.IsNullOrWhiteSpace(model.CustodyNote))
{
description = string.IsNullOrWhiteSpace(description)
? $"Custody note: {model.CustodyNote.Trim()}"
: $"{description.Trim()}\n\nCustody note: {model.CustodyNote.Trim()}";
}
await assets.SaveEventAsync(new AssetEvent
{
StoryAssetID = assetId,
SceneID = model.SceneID,
AssetEventTypeID = eventType.AssetEventTypeID,
FromStateID = model.FromStateID,
ToStateID = model.ToStateID,
EventTitle = title,
EventDescription = description
});
if (model.LocationID.HasValue)
{
await locations.SaveSceneAssetLocationAsync(new SceneAssetLocation
{
SceneID = model.SceneID,
StoryAssetID = assetId,
LocationID = model.LocationID.Value,
Description = title
}, updateCurrentLocation: true);
}
}
public async Task AddAssetDependencyByDropAsync(TimelineAssetDependencyDropViewModel model)
{
if (model.SourceAssetID == model.TargetAssetID)
{
throw new InvalidOperationException("An asset cannot depend on itself.");
}
var existing = await assets.ListDependenciesByAssetAsync(model.SourceAssetID);
if (existing.Any(x => x.SourceAssetID == model.SourceAssetID && x.TargetAssetID == model.TargetAssetID && x.AssetDependencyTypeID == model.AssetDependencyTypeID))
{
throw new InvalidOperationException("That asset dependency already exists.");
}
await assets.SaveDependencyAsync(new AssetDependency
{
SourceAssetID = model.SourceAssetID,
TargetAssetID = model.TargetAssetID,
AssetDependencyTypeID = model.AssetDependencyTypeID,
SourceRequiredStateID = model.SourceRequiredStateID,
TargetBlockedStateID = model.TargetBlockedStateID,
Description = model.Description
});
}
public async Task AddLocationRelationshipByDropAsync(TimelineLocationRelationshipDropViewModel model)
{
if (model.FromLocationID == model.ToLocationID)
{
throw new InvalidOperationException("A location cannot be related to itself.");
}
var existing = await locations.ListRelationshipsByLocationAsync(model.FromLocationID);
if (existing.Any(x => x.FromLocationID == model.FromLocationID && x.ToLocationID == model.ToLocationID && x.LocationRelationshipTypeID == model.LocationRelationshipTypeID))
{
throw new InvalidOperationException("That location relationship already exists.");
}
await locations.SaveRelationshipAsync(new LocationRelationship
{
FromLocationID = model.FromLocationID,
ToLocationID = model.ToLocationID,
LocationRelationshipTypeID = model.LocationRelationshipTypeID,
IsBidirectional = model.IsBidirectional,
Strength = model.Strength,
CanHearNormalSpeech = model.CanHearNormalSpeech,
CanHearLoudNoise = model.CanHearLoudNoise,
CanSeeMovement = model.CanSeeMovement,
CanSeeClearly = model.CanSeeClearly,
CanTravelDirectly = model.CanTravelDirectly,
Notes = model.Notes
});
}
public Task<int> SavePresetAsync(TimelinePresetSaveViewModel model)
{
var filter = model.Filter;
@ -1553,10 +1699,13 @@ public sealed class AssetService(IProjectRepository projects, IAssetRepository a
public async Task<StoryAssetListViewModel?> GetAssetsAsync(int projectId)
{
var project = await projects.GetAsync(projectId);
var lookupData = await assets.GetLookupsAsync();
return project is null ? null : new StoryAssetListViewModel
{
Project = project,
Assets = await assets.ListAssetsAsync(projectId)
Assets = await assets.ListAssetsAsync(projectId),
AssetDependencyTypeOptions = ChapterService.ToSelectList(lookupData.AssetDependencyTypes, x => x.AssetDependencyTypeID, x => x.TypeName),
AssetStateOptions = OptionalStateList(lookupData.AssetStates)
};
}
@ -1754,10 +1903,12 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
public async Task<LocationListViewModel?> GetLocationsAsync(int projectId)
{
var project = await projects.GetAsync(projectId);
var lookupData = await locations.GetLookupsAsync();
return project is null ? null : new LocationListViewModel
{
Project = project,
Locations = await locations.ListByProjectAsync(projectId)
Locations = await locations.ListByProjectAsync(projectId),
RelationshipTypeOptions = ChapterService.ToSelectList(lookupData.RelationshipTypes, x => x.LocationRelationshipTypeID, x => x.TypeName)
};
}

View File

@ -0,0 +1,58 @@
CREATE OR ALTER PROCEDURE dbo.Scene_MoveRelative
@SceneID int,
@AnchorSceneID int,
@Position nvarchar(10)
AS
BEGIN
SET NOCOUNT ON;
IF @SceneID = @AnchorSceneID
RETURN;
DECLARE @SourceChapterID int = (SELECT ChapterID FROM dbo.Scenes WHERE SceneID = @SceneID);
DECLARE @TargetChapterID int;
DECLARE @AnchorSortOrder int;
DECLARE @NewSortOrder int;
SELECT @TargetChapterID = ChapterID, @AnchorSortOrder = SortOrder
FROM dbo.Scenes
WHERE SceneID = @AnchorSceneID AND IsArchived = 0;
IF @TargetChapterID IS NULL
RETURN;
IF @Position = N'Before'
BEGIN
SELECT @NewSortOrder = (ISNULL(MAX(SortOrder), @AnchorSortOrder - 20) + @AnchorSortOrder) / 2
FROM dbo.Scenes
WHERE ChapterID = @TargetChapterID
AND IsArchived = 0
AND SceneID NOT IN (@SceneID, @AnchorSceneID)
AND SortOrder < @AnchorSortOrder;
IF @NewSortOrder >= @AnchorSortOrder
SET @NewSortOrder = @AnchorSortOrder - 5;
END
ELSE
BEGIN
SELECT @NewSortOrder = (@AnchorSortOrder + ISNULL(MIN(SortOrder), @AnchorSortOrder + 20)) / 2
FROM dbo.Scenes
WHERE ChapterID = @TargetChapterID
AND IsArchived = 0
AND SceneID NOT IN (@SceneID, @AnchorSceneID)
AND SortOrder > @AnchorSortOrder;
IF @NewSortOrder <= @AnchorSortOrder
SET @NewSortOrder = @AnchorSortOrder + 5;
END;
UPDATE dbo.Scenes
SET ChapterID = @TargetChapterID,
SortOrder = @NewSortOrder,
UpdatedDate = SYSUTCDATETIME()
WHERE SceneID = @SceneID;
EXEC dbo.Scene_NormalizeSortOrder @SourceChapterID;
EXEC dbo.Scene_NormalizeSortOrder @TargetChapterID;
END;
GO

View File

@ -203,9 +203,15 @@ public sealed class TimelineViewModel
public IReadOnlyList<SelectListItem> PlotLineOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> PlotThreadOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetEventTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetStateOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetDependencyTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CharacterOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CharacterRoleOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> PresenceTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> PovCharacterOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> LocationRelationshipTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> WarningSeverityOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> WarningTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> RevisionStatusOptions { get; set; } = [];
@ -265,6 +271,71 @@ public sealed class TimelinePresetSaveViewModel
public string VisibilityJson { get; set; } = "{}";
}
public sealed class TimelineSceneDropMoveViewModel
{
public int ProjectID { get; set; }
public int? BookID { get; set; }
public int SceneID { get; set; }
public int AnchorSceneID { get; set; }
public string Position { get; set; } = "After";
}
public sealed class TimelineSceneCharacterDropViewModel
{
public int SceneID { get; set; }
public int? CharacterID { get; set; }
public int? RoleInSceneTypeID { get; set; }
public int? PresenceTypeID { get; set; }
public int? LocationID { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class TimelineAssetEventDropViewModel
{
public int SceneID { get; set; }
public int? StoryAssetID { get; set; }
public int AssetEventTypeID { get; set; }
public int? FromStateID { get; set; }
public int? ToStateID { get; set; }
public string EventTitle { get; set; } = string.Empty;
public string? EventDescription { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
public int? LocationID { get; set; }
public string? CustodyNote { get; set; }
}
public sealed class TimelineAssetDependencyDropViewModel
{
public int ProjectID { get; set; }
public int SourceAssetID { get; set; }
public int TargetAssetID { get; set; }
public int AssetDependencyTypeID { get; set; }
public int? SourceRequiredStateID { get; set; }
public int? TargetBlockedStateID { get; set; }
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class TimelineLocationRelationshipDropViewModel
{
public int ProjectID { get; set; }
public int FromLocationID { get; set; }
public int ToLocationID { get; set; }
public int LocationRelationshipTypeID { get; set; }
public bool IsBidirectional { get; set; }
public int? Strength { get; set; }
public bool CanHearNormalSpeech { get; set; }
public bool CanHearLoudNoise { get; set; }
public bool CanSeeMovement { get; set; }
public bool CanSeeClearly { get; set; }
public bool CanTravelDirectly { get; set; }
public string? Notes { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class BookTimelineViewModel
{
public Book Book { get; set; } = new();
@ -421,6 +492,8 @@ public sealed class StoryAssetListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<StoryAsset> Assets { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetDependencyTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetStateOptions { get; set; } = [];
}
public sealed class StoryAssetEditViewModel
@ -674,6 +747,7 @@ public sealed class LocationListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<LocationItem> Locations { get; set; } = [];
public IReadOnlyList<SelectListItem> RelationshipTypeOptions { get; set; } = [];
}
public sealed class LocationEditViewModel

View File

@ -37,7 +37,7 @@
<div class="asset-card-grid">
@foreach (var character in Model.Characters)
{
<article class="asset-card character-card">
<article class="asset-card character-card" draggable="true" data-drag-type="character" data-drag-id="@character.CharacterID" data-drag-label="@character.CharacterName">
<div>
<p class="eyebrow">Character</p>
<h2><a asp-action="Details" asp-route-id="@character.CharacterID">@character.CharacterName</a></h2>

View File

@ -24,6 +24,38 @@
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">New location</a>
</div>
<section class="drag-drop-panel d-none" data-drag-drop-panel>
<div class="drag-drop-panel-header">
<div>
<p class="eyebrow" data-drag-panel-kicker>Location relationship</p>
<h2 data-drag-panel-title>Create location relationship</h2>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" data-drag-cancel>Cancel</button>
</div>
<p class="muted" data-drag-panel-summary></p>
<form asp-controller="Locations" asp-action="DropRelationship" method="post" class="drag-drop-form" data-drag-form="location-relationship">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="FromLocationID" />
<input type="hidden" name="ToLocationID" />
<div class="row g-2">
<div class="col-12"><label class="form-label">Relationship type</label><select class="form-select form-select-sm" name="LocationRelationshipTypeID" asp-items="Model.RelationshipTypeOptions"></select></div>
<div class="col-12 drag-check-grid">
<label><input type="checkbox" name="IsBidirectional" value="true" /> Bidirectional</label>
<label><input type="checkbox" name="CanHearNormalSpeech" value="true" /> Can hear speech</label>
<label><input type="checkbox" name="CanHearLoudNoise" value="true" /> Can hear loud noise</label>
<label><input type="checkbox" name="CanSeeMovement" value="true" /> Can see movement</label>
<label><input type="checkbox" name="CanSeeClearly" value="true" /> Can see clearly</label>
<label><input type="checkbox" name="CanTravelDirectly" value="true" /> Can travel directly</label>
</div>
<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Notes" rows="2"></textarea></div>
</div>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Save</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
</section>
@if (!Model.Locations.Any())
{
<section class="empty-panel">
@ -36,7 +68,7 @@ else
<section class="edit-panel location-tree">
@foreach (var location in Model.Locations)
{
<div class="location-tree-row" style="--location-depth:@location.Depth">
<div class="location-tree-row" draggable="true" data-drag-type="location" data-drag-id="@location.LocationID" data-drag-label="@location.LocationName" style="--location-depth:@location.Depth">
<div>
<a asp-action="Details" asp-route-id="@location.LocationID"><strong>@location.LocationName</strong></a>
@if (!string.IsNullOrWhiteSpace(location.LocationTypeName))

View File

@ -27,6 +27,33 @@
</div>
</div>
<section class="drag-drop-panel d-none" data-drag-drop-panel>
<div class="drag-drop-panel-header">
<div>
<p class="eyebrow" data-drag-panel-kicker>Asset dependency</p>
<h2 data-drag-panel-title>Create asset dependency</h2>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" data-drag-cancel>Cancel</button>
</div>
<p class="muted" data-drag-panel-summary></p>
<form asp-controller="StoryAssets" asp-action="DropDependency" method="post" class="drag-drop-form" data-drag-form="asset-dependency">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="SourceAssetID" />
<input type="hidden" name="TargetAssetID" />
<div class="row g-2">
<div class="col-12"><label class="form-label">Dependency type</label><select class="form-select form-select-sm" name="AssetDependencyTypeID" asp-items="Model.AssetDependencyTypeOptions"></select></div>
<div class="col-6"><label class="form-label">Source required state</label><select class="form-select form-select-sm" name="SourceRequiredStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
<div class="col-6"><label class="form-label">Target blocked state</label><select class="form-select form-select-sm" name="TargetBlockedStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Description" rows="2"></textarea></div>
</div>
<p class="plain-preview" data-asset-dependency-preview></p>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Save Dependency</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
</section>
<section class="list-section">
@if (!Model.Assets.Any())
{
@ -37,7 +64,7 @@
<div class="asset-card-grid">
@foreach (var asset in Model.Assets)
{
<article class="asset-card">
<article class="asset-card" draggable="true" data-drag-type="asset" data-drag-id="@asset.StoryAssetID" data-drag-label="@asset.AssetName" data-asset-drop-target="true" data-asset-id="@asset.StoryAssetID" data-asset-label="@asset.AssetName">
<div>
<p class="eyebrow">@asset.KindName</p>
<h2><a asp-action="Details" asp-route-id="@asset.StoryAssetID">@asset.AssetName</a></h2>

View File

@ -179,8 +179,9 @@
</form>
</details>
<div class="timeline-visibility-panel">
<span class="eyebrow">Visibility</span>
<details class="timeline-control-group">
<summary>View</summary>
<div class="timeline-visibility-panel">
<label class="timeline-control-select">Density
<select class="form-select form-select-sm" data-timeline-density>
<option value="compact">Compact</option>
@ -202,10 +203,12 @@
<label><input type="checkbox" data-timeline-toggle="locations" checked /> Locations</label>
<label><input type="checkbox" data-timeline-toggle="warnings" checked /> Warnings</label>
<label><input type="checkbox" data-timeline-toggle="metrics" checked /> Metrics</label>
</div>
</div>
</details>
<div class="timeline-jump-panel">
<span class="eyebrow">Jump / search</span>
<details class="timeline-control-group">
<summary>Jump</summary>
<div class="timeline-jump-panel">
<input class="form-control form-control-sm" type="search" placeholder="Search timeline..." data-timeline-search />
<select class="form-select form-select-sm" data-timeline-jump>
<option value="">Jump to scene</option>
@ -259,9 +262,12 @@
}
</select>
<span class="timeline-shortcut-hint">Shortcuts: arrows scroll, +/- zoom, Esc clears focus</span>
</div>
</div>
</details>
<div class="timeline-preset-panel">
<details class="timeline-control-group">
<summary>Saved Views</summary>
<div class="timeline-preset-panel">
<form asp-action="SavePreset" method="post" data-timeline-preset-form>
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="BookID" value="@Model.SelectedBookID" />
@ -308,7 +314,8 @@
}
</div>
}
</div>
</div>
</details>
</section>
@if (!string.IsNullOrWhiteSpace(Model.FocusLabel))
@ -320,6 +327,32 @@
</section>
}
@if (TempData["TimelineDragMessage"] is string dragMessage)
{
<section class="timeline-focus-banner drag-message">
<strong>@dragMessage</strong>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Review warnings</a>
</section>
}
<details class="timeline-drag-palette" data-drag-palette>
<summary>Drag Sources</summary>
<div class="drag-chip-group">
@foreach (var character in Model.CharacterOptions.Take(10))
{
<span class="drag-chip character" draggable="true" data-drag-type="character" data-drag-id="@character.Value" data-drag-label="@character.Text">@character.Text</span>
}
@foreach (var asset in Model.AssetOptions.Take(10))
{
<span class="drag-chip asset" draggable="true" data-drag-type="asset" data-drag-id="@asset.Value" data-drag-label="@asset.Text">@asset.Text</span>
}
@foreach (var location in Model.LocationOptions.Take(10))
{
<span class="drag-chip location" draggable="true" data-drag-type="location" data-drag-id="@location.Value" data-drag-label="@location.Text">@location.Text</span>
}
</div>
</details>
@if (Model.Books.Any())
{
<section class="timeline-minimap" data-timeline-minimap>
@ -339,6 +372,110 @@
</section>
}
<section class="drag-drop-panel d-none" data-drag-drop-panel>
<div class="drag-drop-panel-header">
<div>
<p class="eyebrow" data-drag-panel-kicker>Drag action</p>
<h2 data-drag-panel-title>Confirm action</h2>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" data-drag-cancel>Cancel</button>
</div>
<p class="muted" data-drag-panel-summary></p>
<form asp-controller="Timeline" asp-action="DropMoveScene" method="post" class="drag-drop-form d-none" data-drag-form="scene">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="BookID" value="@Model.SelectedBookID" />
<input type="hidden" name="SceneID" />
<input type="hidden" name="AnchorSceneID" />
<input type="hidden" name="Position" />
<div class="focused-validation-summary">
<span>After moving, PlotLine will run continuity validation.</span>
<span>Existing links, events, metrics, purposes, characters, assets, warnings and dependencies stay attached to the same SceneID.</span>
</div>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Move Scene</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Review Warnings</a>
</div>
</form>
<form asp-controller="Timeline" asp-action="DropCharacter" method="post" class="drag-drop-form d-none" data-drag-form="character">
<input type="hidden" name="SceneID" />
<input type="hidden" name="CharacterID" />
<input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
<div class="row g-2">
<div class="col-6"><label class="form-label">Role in scene</label><select class="form-select form-select-sm" name="RoleInSceneTypeID" asp-items="Model.CharacterRoleOptions"><option value="">Not set</option></select></div>
<div class="col-6"><label class="form-label">Presence type</label><select class="form-select form-select-sm" name="PresenceTypeID" asp-items="Model.PresenceTypeOptions"><option value="">Not set</option></select></div>
<div class="col-12"><label class="form-label">Current location</label><select class="form-select form-select-sm" name="LocationID" asp-items="Model.LocationOptions"><option value="">Optional</option></select></div>
</div>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Add Character</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
<form asp-controller="Timeline" asp-action="DropAssetEvent" method="post" class="drag-drop-form d-none" data-drag-form="asset-event">
<input type="hidden" name="SceneID" />
<input type="hidden" name="StoryAssetID" />
<input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
<div class="row g-2">
<div class="col-6"><label class="form-label">Event type</label><select class="form-select form-select-sm" name="AssetEventTypeID" asp-items="Model.AssetEventTypeOptions"></select></div>
<div class="col-6"><label class="form-label">State transition</label><select class="form-select form-select-sm" name="ToStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
<div class="col-12"><label class="form-label">Location</label><select class="form-select form-select-sm" name="LocationID" asp-items="Model.LocationOptions"><option value="">Optional</option></select></div>
<div class="col-12"><label class="form-label">Event title</label><input class="form-control form-control-sm" name="EventTitle" /></div>
<div class="col-12"><label class="form-label">Custody note</label><textarea class="form-control form-control-sm" name="CustodyNote" rows="2"></textarea></div>
</div>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Add Asset Event</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
<form asp-controller="Timeline" asp-action="DropAssetDependency" method="post" class="drag-drop-form d-none" data-drag-form="asset-dependency">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
<input type="hidden" name="SourceAssetID" />
<input type="hidden" name="TargetAssetID" />
<div class="row g-2">
<div class="col-12"><label class="form-label">Dependency type</label><select class="form-select form-select-sm" name="AssetDependencyTypeID" asp-items="Model.AssetDependencyTypeOptions"></select></div>
<div class="col-6"><label class="form-label">Source required state</label><select class="form-select form-select-sm" name="SourceRequiredStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
<div class="col-6"><label class="form-label">Target blocked state</label><select class="form-select form-select-sm" name="TargetBlockedStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Description" rows="2"></textarea></div>
</div>
<p class="plain-preview" data-asset-dependency-preview></p>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Save Dependency</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
<form asp-controller="Timeline" asp-action="DropLocationRelationship" method="post" class="drag-drop-form d-none" data-drag-form="location-relationship">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
<input type="hidden" name="FromLocationID" />
<input type="hidden" name="ToLocationID" />
<div class="row g-2">
<div class="col-12"><label class="form-label">Relationship type</label><select class="form-select form-select-sm" name="LocationRelationshipTypeID" asp-items="Model.LocationRelationshipTypeOptions"></select></div>
<div class="col-12 drag-check-grid">
<label><input type="checkbox" name="IsBidirectional" value="true" /> Bidirectional</label>
<label><input type="checkbox" name="CanHearNormalSpeech" value="true" /> Can hear speech</label>
<label><input type="checkbox" name="CanHearLoudNoise" value="true" /> Can hear loud noise</label>
<label><input type="checkbox" name="CanSeeMovement" value="true" /> Can see movement</label>
<label><input type="checkbox" name="CanSeeClearly" value="true" /> Can see clearly</label>
<label><input type="checkbox" name="CanTravelDirectly" value="true" /> Can travel directly</label>
</div>
<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Notes" rows="2"></textarea></div>
</div>
<div class="button-row mt-2">
<button class="btn btn-primary btn-sm" type="submit">Save</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
</div>
</form>
</section>
@if (!Model.Books.Any())
{
<section class="empty-panel">
@ -350,7 +487,7 @@ else
{
<div class="workspace-layout timeline-filtered-root" data-timeline-root>
<main class="timeline-workspace">
<section class="timeline-shell workspace-timeline" data-timeline-section="scene-cards">
<section class="timeline-shell workspace-timeline" data-timeline-section="scene-cards" style="--scene-count:@Model.OrderedScenes.Count">
<div class="timeline-section-header" data-collapse-target="scene-cards">
<button type="button" data-collapse-toggle="scene-cards">Scene Cards</button>
<span>@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
@ -392,7 +529,13 @@ else
var warningSearch = scene.WarningCount > 0 ? "warning warnings continuity" : "";
var searchText = $"{scene.SceneNumber} {scene.SceneTitle} {scene.Summary} {povLabel} {scene.PrimaryLocationName} {string.Join(' ', scene.PurposeLabels.Select(x => x.PurposeName))} {plotSearch} {assetSearch} {characterSearch} {warningSearch}";
<a class="timeline-scene-card @selectedClass @matchClass @focusClass"
draggable="true"
data-scene-id="@scene.SceneID"
data-drag-type="scene"
data-drag-id="@scene.SceneID"
data-drag-label="Scene @scene.SceneNumber: @scene.SceneTitle"
data-chapter-id="@chapter.Chapter.ChapterID"
data-chapter-label="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle"
data-timeline-search-text="@searchText"
asp-controller="Timeline"
asp-action="Index"
@ -453,7 +596,7 @@ else
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
{
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<div class="lane-board-heading plot-lane-board-heading">
<button type="button" data-collapse-toggle="plot-lines">Plot line lanes</button>
<small>Thread event markers by scene</small>
</div>
@ -515,7 +658,7 @@ else
@if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
{
<div class="asset-lane-board" data-timeline-section="assets" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<div class="lane-board-heading asset-lane-board-heading">
<button type="button" data-collapse-toggle="assets">Story asset lanes</button>
<small>Asset event markers by scene</small>
</div>
@ -530,7 +673,7 @@ else
if ((lane.Asset.Importance >= 8 ? "High importance" : "Other assets") != group.Key) { continue; }
var assetFocusClass = Model.Filter.FocusType == "asset" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Asset.StoryAssetID ? "lane-dim" : "";
<div class="asset-lane-row @assetFocusClass">
<a class="asset-lane-label" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
<a class="asset-lane-label" draggable="true" data-drag-type="asset" data-drag-id="@lane.Asset.StoryAssetID" data-drag-label="@lane.Asset.AssetName" data-asset-drop-target="true" data-asset-id="@lane.Asset.StoryAssetID" data-asset-label="@lane.Asset.AssetName" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
<strong>@lane.Asset.AssetName</strong>
<span>@lane.Asset.KindName / @lane.Asset.Importance</span>
</a>
@ -572,7 +715,7 @@ else
@if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
{
<div class="character-lane-board" data-timeline-section="characters" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<div class="lane-board-heading character-lane-board-heading">
<button type="button" data-collapse-toggle="characters">Character appearances</button>
<small>First twelve project characters</small>
</div>

View File

@ -497,19 +497,31 @@ a {
padding-top: 14px;
}
.lane-board-heading,
.plot-lane-board-heading {
position: sticky;
left: 0;
display: grid;
width: fit-content;
z-index: 7;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
width: calc(var(--timeline-label-width) + (var(--scene-count) * var(--timeline-scene-width)) + (var(--scene-count) * var(--timeline-grid-gap)));
min-width: 100%;
margin-bottom: 2px;
border-radius: 8px;
padding: 8px 10px;
color: var(--plotline-accent-dark);
background: rgba(255, 255, 255, 0.92);
font-weight: 700;
box-shadow: 0 8px 16px rgba(30, 37, 43, 0.05);
}
.lane-board-heading small,
.plot-lane-board-heading small {
color: var(--plotline-muted);
font-weight: 500;
white-space: nowrap;
}
.plot-lane-row {
@ -1735,21 +1747,133 @@ a {
opacity: 0.75;
}
.timeline-drag-palette,
.drag-drop-panel {
border: 1px solid rgba(47, 111, 99, 0.18);
border-radius: 10px;
margin-bottom: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 10px 24px rgba(55, 48, 39, 0.05);
}
.timeline-drag-palette {
display: grid;
grid-template-columns: minmax(220px, 0.35fr) minmax(0, 1fr);
gap: 12px;
align-items: center;
}
.drag-chip-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.drag-chip {
display: inline-flex;
align-items: center;
border: 1px solid var(--plotline-line);
border-radius: 999px;
padding: 4px 9px;
background: #fff;
color: var(--plotline-ink);
font-size: 0.78rem;
font-weight: 800;
cursor: grab;
}
.drag-chip.character {
border-color: #dacb9e;
background: #fff8e8;
}
.drag-chip.asset {
border-color: #ded9ea;
background: #f4f0ff;
}
.drag-chip.location {
border-color: rgba(47, 111, 99, 0.18);
background: #eef8f4;
}
.is-dragging {
opacity: 0.55;
}
.drop-target-active {
outline: 3px solid rgba(47, 111, 99, 0.28);
outline-offset: 2px;
}
.timeline-scene-card.drop-before {
box-shadow: inset 5px 0 0 #c58b2b, 0 12px 26px rgba(197, 139, 43, 0.16);
}
.timeline-scene-card.drop-after {
box-shadow: inset -5px 0 0 #c58b2b, 0 12px 26px rgba(197, 139, 43, 0.16);
}
.drag-drop-panel {
position: sticky;
top: 132px;
z-index: 19;
}
.drag-drop-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.drag-drop-panel h2 {
margin: 0;
font-size: 1.08rem;
}
.drag-drop-form {
border-top: 1px solid var(--plotline-line);
margin-top: 10px;
padding-top: 10px;
}
.plain-preview {
border: 1px solid rgba(197, 139, 43, 0.28);
border-radius: 8px;
margin: 10px 0 0;
padding: 8px 10px;
color: #5e4518;
background: #fff8e8;
font-size: 0.86rem;
font-weight: 800;
}
.drag-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px 10px;
color: var(--plotline-muted);
font-size: 0.82rem;
font-weight: 700;
}
.timeline-section-header,
.plot-lane-board-heading,
.lane-board-heading,
.pacing-heading {
position: sticky;
left: 0;
z-index: 7;
}
.timeline-section-header,
.plot-lane-board-heading {
.timeline-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: var(--timeline-label-width);
width: calc(var(--timeline-label-width) + (var(--scene-count) * var(--timeline-scene-width)) + (var(--scene-count) * var(--timeline-grid-gap)));
min-width: 100%;
margin-bottom: 10px;
border-radius: 8px;
padding: 8px 10px;
@ -1758,6 +1882,7 @@ a {
}
.timeline-section-header button,
.lane-board-heading button,
.plot-lane-board-heading button,
.pacing-heading button,
.lane-group-heading {
@ -1771,6 +1896,7 @@ a {
}
.timeline-section-header button::before,
.lane-board-heading button::before,
.plot-lane-board-heading button::before,
.pacing-heading button::before,
.lane-group-heading::before {
@ -1782,6 +1908,7 @@ a {
}
.timeline-section-header button.is-collapsed::before,
.lane-board-heading button.is-collapsed::before,
.plot-lane-board-heading button.is-collapsed::before,
.pacing-heading button.is-collapsed::before,
.lane-group-heading.is-collapsed::before {
@ -1905,6 +2032,204 @@ a {
min-height: 230px;
}
/* Timeline stabilization: keep controls compact and story lanes readable. */
.timeline-control-panel {
position: relative;
top: auto;
z-index: 4;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 8px;
box-shadow: none;
}
.timeline-filter-drawer,
.timeline-control-group {
border: 1px solid var(--plotline-line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
}
.timeline-filter-drawer[open],
.timeline-control-group[open] {
border-radius: 10px;
flex-basis: 100%;
}
.timeline-filter-drawer summary,
.timeline-control-group summary {
min-width: max-content;
padding: 5px 10px;
color: var(--plotline-accent-dark);
font-size: 0.82rem;
font-weight: 900;
cursor: pointer;
list-style: none;
}
.timeline-filter-drawer summary::-webkit-details-marker,
.timeline-control-group summary::-webkit-details-marker,
.timeline-drag-palette summary::-webkit-details-marker {
display: none;
}
.timeline-filter-drawer summary::after,
.timeline-control-group summary::after,
.timeline-drag-palette summary::after {
content: " v";
color: var(--plotline-muted);
font-size: 0.72rem;
}
.timeline-filter-drawer[open] summary::after,
.timeline-control-group[open] summary::after,
.timeline-drag-palette[open] summary::after {
content: " ^";
}
.timeline-filter-grid {
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
gap: 8px;
max-height: min(48vh, 430px);
overflow: auto;
padding: 0 10px 10px;
}
.timeline-filter-grid .form-label {
margin-bottom: 2px;
font-size: 0.72rem;
font-weight: 800;
}
.timeline-filter-checks,
.timeline-filter-actions {
grid-column: 1 / -1;
}
.timeline-visibility-panel,
.timeline-jump-panel,
.timeline-preset-panel {
padding: 0 10px 10px;
}
.timeline-jump-panel {
grid-template-columns: minmax(180px, 1.5fr) repeat(3, minmax(150px, 1fr));
}
.timeline-shortcut-hint {
grid-column: 1 / -1;
}
.timeline-preset-panel form {
flex-wrap: nowrap;
}
.timeline-preset-panel .form-control {
width: 150px;
}
.timeline-preset-panel .btn,
.timeline-filter-actions .btn {
white-space: nowrap;
}
.timeline-drag-palette {
display: block;
margin-bottom: 8px;
padding: 7px 9px;
box-shadow: none;
}
.timeline-drag-palette summary {
color: var(--plotline-accent-dark);
font-size: 0.82rem;
font-weight: 900;
cursor: pointer;
}
.timeline-drag-palette .drag-chip-group {
margin-top: 8px;
}
.drag-drop-panel {
position: relative;
top: auto;
z-index: 9;
box-shadow: 0 12px 28px rgba(30, 37, 43, 0.08);
}
.timeline-filtered-root,
.timeline-filtered-root.density-compact,
.timeline-filtered-root.zoom-book {
--timeline-scene-width: 220px;
--timeline-grid-gap: 10px;
}
.timeline-filtered-root.density-expanded,
.timeline-filtered-root.zoom-scene-detail {
--timeline-scene-width: 290px;
}
.timeline-filtered-root.density-compact.zoom-book {
--timeline-scene-width: 200px;
}
.timeline-chapter,
.timeline-scene-card,
.timeline-empty-scene {
overflow-wrap: normal;
word-break: normal;
}
.timeline-scene-card strong,
.timeline-chapter-title strong,
.plot-lane-label strong,
.asset-lane-label strong,
.character-lane-label strong {
min-width: 0;
white-space: normal;
word-break: normal;
overflow-wrap: break-word;
}
.timeline-scene-card {
cursor: grab;
}
.timeline-scene-card:active,
.drag-chip:active,
[draggable="true"]:active {
cursor: grabbing;
}
body.drag-active .timeline-scene-card,
body.dragging-asset [data-asset-drop-target],
body.dragging-location [data-drag-type="location"] {
transition: outline-color 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
}
body.dragging-character .timeline-scene-card,
body.dragging-asset .timeline-scene-card,
body.dragging-scene .timeline-scene-card,
body.dragging-asset [data-asset-drop-target],
body.dragging-location [data-drag-type="location"] {
outline: 1px dashed rgba(47, 111, 99, 0.22);
outline-offset: 2px;
}
.is-dragging {
opacity: 0.42;
transform: scale(0.98);
}
.drop-target-active {
outline: 3px solid rgba(47, 111, 99, 0.42) !important;
outline-offset: 3px;
}
.analytics-metric-row {
display: grid;
grid-template-columns: 170px minmax(560px, 1fr);

View File

@ -529,3 +529,213 @@
}
});
})();
(() => {
const root = document.querySelector("[data-timeline-root]");
const panel = document.querySelector("[data-drag-drop-panel]");
if (!panel) {
return;
}
const forms = new Map([...panel.querySelectorAll("[data-drag-form]")].map((form) => [form.dataset.dragForm, form]));
const title = panel.querySelector("[data-drag-panel-title]");
const kicker = panel.querySelector("[data-drag-panel-kicker]");
const summary = panel.querySelector("[data-drag-panel-summary]");
let currentDrag = null;
const setValue = (form, name, value) => {
const field = form?.querySelector(`[name='${CSS.escape(name)}']`);
if (field) {
field.value = value ?? "";
}
};
const selectText = (select) => select?.selectedOptions?.[0]?.textContent?.trim() || "";
const showForm = (key, heading, body, actionLabel) => {
forms.forEach((form, formKey) => form.classList.toggle("d-none", formKey !== key));
panel.classList.remove("d-none");
title.textContent = heading;
kicker.textContent = actionLabel;
summary.textContent = body;
panel.scrollIntoView({ behavior: "smooth", block: "center" });
};
const hidePanel = () => {
panel.classList.add("d-none");
forms.forEach((form) => form.classList.add("d-none"));
};
const readPayload = (event) => {
try {
const payload = JSON.parse(event.dataTransfer.getData("application/json") || "{}");
return payload.type ? payload : currentDrag || {};
} catch {
return currentDrag || {};
}
};
const writePayload = (event, element) => {
const payload = {
type: element.dataset.dragType,
id: element.dataset.dragId,
label: element.dataset.dragLabel || element.textContent.trim()
};
event.dataTransfer.effectAllowed = "copyMove";
event.dataTransfer.setData("application/json", JSON.stringify(payload));
event.dataTransfer.setData("text/plain", `${payload.type}:${payload.id}`);
currentDrag = payload;
document.body.classList.add("drag-active", `dragging-${payload.type}`);
element.classList.add("is-dragging");
};
document.querySelectorAll("[data-drag-type][draggable='true']").forEach((item) => {
item.addEventListener("dragstart", (event) => writePayload(event, item));
item.addEventListener("dragend", () => {
item.classList.remove("is-dragging");
document.body.classList.remove("drag-active", "dragging-scene", "dragging-character", "dragging-asset", "dragging-location");
currentDrag = null;
document.querySelectorAll(".drop-target-active, .drop-before, .drop-after").forEach((target) => {
target.classList.remove("drop-target-active", "drop-before", "drop-after");
});
});
});
panel.querySelectorAll("[data-drag-cancel]").forEach((button) => button.addEventListener("click", hidePanel));
root?.querySelectorAll(".timeline-scene-card[data-scene-id]").forEach((card) => {
card.addEventListener("dragover", (event) => {
const payload = readPayload(event);
if (["scene", "character", "asset"].includes(payload.type)) {
event.preventDefault();
event.dataTransfer.dropEffect = payload.type === "scene" ? "move" : "copy";
card.classList.add("drop-target-active");
card.classList.toggle("drop-before", payload.type === "scene" && event.offsetX < card.clientWidth / 2);
card.classList.toggle("drop-after", payload.type === "scene" && event.offsetX >= card.clientWidth / 2);
}
});
card.addEventListener("dragleave", () => card.classList.remove("drop-target-active", "drop-before", "drop-after"));
card.addEventListener("drop", (event) => {
const payload = readPayload(event);
if (!["scene", "character", "asset"].includes(payload.type)) {
return;
}
event.preventDefault();
card.classList.remove("drop-target-active", "drop-before", "drop-after");
const sceneId = card.dataset.sceneId;
if (payload.type === "scene") {
if (payload.id === sceneId) {
showForm("scene", "Cannot move scene", "Drop a scene before or after a different scene.", "Scene move");
return;
}
const form = forms.get("scene");
const position = event.offsetX < card.clientWidth / 2 ? "Before" : "After";
setValue(form, "SceneID", payload.id);
setValue(form, "AnchorSceneID", sceneId);
setValue(form, "Position", position);
showForm("scene", "Move scene?", `Move ${payload.label} ${position.toLowerCase()} ${card.dataset.dragLabel}.`, "Scene move");
return;
}
if (payload.type === "character") {
const form = forms.get("character");
setValue(form, "SceneID", sceneId);
setValue(form, "CharacterID", payload.id);
showForm("character", "Add character to scene", `Add ${payload.label} to ${card.dataset.dragLabel}.`, "Character drop");
return;
}
const form = forms.get("asset-event");
setValue(form, "SceneID", sceneId);
setValue(form, "StoryAssetID", payload.id);
setValue(form, "EventTitle", "");
showForm("asset-event", "What happens to this asset?", `Add an event for ${payload.label} in ${card.dataset.dragLabel}.`, "Asset drop");
});
});
const assetDependencyForm = forms.get("asset-dependency");
const updateAssetPreview = () => {
const source = assetDependencyForm?.dataset.sourceLabel || "Source asset";
const target = assetDependencyForm?.dataset.targetLabel || "Target asset";
const dependency = selectText(assetDependencyForm?.querySelector("[name='AssetDependencyTypeID']")) || "Requires";
const sourceState = selectText(assetDependencyForm?.querySelector("[name='SourceRequiredStateID']")) || "the required state";
const targetState = selectText(assetDependencyForm?.querySelector("[name='TargetBlockedStateID']")) || "the blocked state";
const preview = panel.querySelector("[data-asset-dependency-preview]");
if (preview) {
preview.textContent = `${source} ${dependency.toLowerCase()} ${target}. ${source} should be ${sourceState} before ${target} can be ${targetState}.`;
}
};
assetDependencyForm?.querySelectorAll("select").forEach((select) => select.addEventListener("change", updateAssetPreview));
document.querySelectorAll("[data-asset-drop-target], .drag-chip.asset").forEach((target) => {
target.addEventListener("dragover", (event) => {
const payload = readPayload(event);
if (payload.type === "asset") {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
target.classList.add("drop-target-active");
}
});
target.addEventListener("dragleave", () => target.classList.remove("drop-target-active"));
target.addEventListener("drop", (event) => {
const payload = readPayload(event);
const targetId = target.dataset.assetId || target.dataset.dragId;
const targetLabel = target.dataset.assetLabel || target.dataset.dragLabel || target.textContent.trim();
if (payload.type !== "asset" || !targetId) {
return;
}
event.preventDefault();
target.classList.remove("drop-target-active");
if (payload.id === targetId) {
showForm("asset-dependency", "Cannot create dependency", "Drop one asset onto a different asset.", "Asset dependency");
return;
}
setValue(assetDependencyForm, "SourceAssetID", payload.id);
setValue(assetDependencyForm, "TargetAssetID", targetId);
assetDependencyForm.dataset.sourceLabel = payload.label;
assetDependencyForm.dataset.targetLabel = targetLabel;
updateAssetPreview();
showForm("asset-dependency", "Create asset dependency", `${payload.label} -> ${targetLabel}`, "Asset dependency");
});
});
document.querySelectorAll("[data-drag-type='location']").forEach((target) => {
target.addEventListener("dragover", (event) => {
const payload = readPayload(event);
if (payload.type === "location") {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
target.classList.add("drop-target-active");
}
});
target.addEventListener("dragleave", () => target.classList.remove("drop-target-active"));
target.addEventListener("drop", (event) => {
const payload = readPayload(event);
const targetId = target.dataset.dragId;
const targetLabel = target.dataset.dragLabel || target.textContent.trim();
if (payload.type !== "location") {
return;
}
event.preventDefault();
target.classList.remove("drop-target-active");
if (payload.id === targetId) {
showForm("location-relationship", "Cannot create relationship", "Drop one location onto a different location.", "Location relationship");
return;
}
const form = forms.get("location-relationship");
setValue(form, "FromLocationID", payload.id);
setValue(form, "ToLocationID", targetId);
showForm("location-relationship", "Create location relationship", `${payload.label} -> ${targetLabel}`, "Location drop");
});
});
})();